import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.junit.rules.MethodRule;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;

import static java.util.concurrent.CompletableFuture.completedFuture;

public class JsaCrash implements TestRule {
    public static final String TEST_CASE_CONTEXT = "Test Case";

    private final Map<String, Future<Object>> contexts;

    private final CompletableFuture<Void> contextsStart = new CompletableFuture<>();
    private final CompletableFuture<Void> shutdownStart;
    private final Map<String, CompletableFuture<Void>> shutdownEnd;

    private final List<? extends TestRule> innerRules;

    JsaCrash(final Object testCase,
            final Map<String, Supplier<CompletableFuture<Object>>> contextSuppliers,
            final List<? extends TestRule> innerRules,
            final CompletableFuture<Void> shutdownStart,
            final Map<String, CompletableFuture<Void>> shutdownEnd)
    {
        this.innerRules = innerRules;
        this.shutdownStart = shutdownStart;
        this.shutdownEnd = shutdownEnd;
        final var myContexts = new HashMap<String, Future<Object>>();
        contextSuppliers.forEach((name, supplier) -> {
            myContexts.put(name, contextsStart.thenCompose(x -> start(name, supplier)));
        });
        this.contexts = Map.copyOf(myContexts);
    }

    public static void main(final String... args) throws Throwable {
        JsaCrash.builder()
                .launch("test").as(TestConfiguration.class)
                .build()
                .apply(new Statement() {
                    @Override
                    public void evaluate() throws Throwable {
                        System.out.println("statement"); // NOPMD
                    }
                }, Description.EMPTY)
                .evaluate();
        System.out.println("done!"); // NOPMD
    }

    public static class TestConfiguration {
    }

    @Override
    public Statement apply(final Statement base, final Description description) {
        Statement stmt = new Statement() {
            @Override
            public void evaluate() throws Throwable {
                try {
                    start();
                    base.evaluate();
                } finally {
                    stop();
                }
            }
        };
        for (final TestRule r : innerRules) {
            stmt = r.apply(stmt, description);
        }
        return stmt;
    }

    protected void start() throws Throwable {
        contextsStart.complete(null);
        contexts.keySet()
            .forEach(this::context);
    }

    protected void stop() {
        shutdownStart.complete(null);
        try {
            CompletableFuture.allOf(shutdownEnd.values().toArray(CompletableFuture<?>[]::new)).get(30, TimeUnit.SECONDS);
        } catch (final InterruptedException e1) {
        } catch (final ExecutionException e1) {
            shutdownEnd.forEach((ctxName, cf) -> {
                cf.whenComplete((x, e) -> {
                    if (e != null) {
                    }
                });
            });
        } catch (final TimeoutException e1) {
        }
    }

    private CompletableFuture<Object> start(final String name, final Supplier<CompletableFuture<Object>> starter) {
        return starter.get();
    }

    public Object context(final String ctxName) {
        try {
            return Optional.ofNullable(contexts.get(ctxName))
                    .orElse(null)
                    .get();
        } catch (final InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } catch (final ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    public MethodRule injectRule() {
        return new MethodRule() {
            @Override
            public Statement apply(final Statement base, final FrameworkMethod method, final Object target) {
                return new Statement() {
                    @Override
                    public void evaluate() throws Throwable {
                        base.evaluate();
                    }
                };
            }
        };
    }

    public static JsaCrash.Builder builder() {
        return new Builder();
    }

    public static final class Builder {
        private final Map<String, Supplier<CompletableFuture<Object>>> contextSuppliers = new LinkedHashMap<>();
        private final Map<String, Object> baseProperties = new HashMap<>();
        private final List<TestRule> innerRules = new ArrayList<>();
        private final CompletableFuture<Void> shutdownStart = new CompletableFuture<>();
        private final Map<String, CompletableFuture<Void>> shutdownEnd = new LinkedHashMap<>();
        private JsaCrash builtRule;

        public Builder() {
        }

        public ContextBuilder launch(final String ctxName) {
            return new ContextBuilder(ctxName);
        }

        Class<?>[] featureConfiguration(final ClassLoader cl) {
            return new Class<?>[0];
        }

        Builder addContext(final ContextBuilder ctx, final ResolvedService service) {
            URLClassLoader cl;
            try {
                cl = new URLClassLoader(extractAndCacheClasspath(service.tarball()).toArray(URL[]::new));
            } catch (final IOException e) {
                throw new UncheckedIOException(e);
            }
            return addContext(ctx, () -> {
                final var myProps = new HashMap<>(ctx.properties);
                try {
                    final String mainClassName = service.spec().mainClass()
                            .orElse("main");
                    final var mainClass = Class.forName(mainClassName, true, cl);
                    return null;
                } catch (final ReflectiveOperationException e) {
                    throw new RuntimeException("Context '" + ctx.ctxName + "' failed startup", e);
                }
            }, t -> t.setContextClassLoader(cl));
        }

        static List<URL> extractAndCacheClasspath(final Path tarball) throws IOException {
            final var results = new ArrayList<URL>();
            return results;
        }

        Builder addContext(final ContextBuilder ctx, final Class<?> configurationClass) {
            return addContext(ctx, () -> {return null;}, t -> {});
        }

        private Builder addContext(final ContextBuilder ctx, final Supplier<Object> runner, final Consumer<Thread> threadCustomizer) {
            final var shutdown = new CompletableFuture<Void>();
            shutdownEnd.put(ctx.ctxName, shutdown);
            contextSuppliers.put(ctx.ctxName, () -> {
                final var startup = new CompletableFuture<Object>();
                final var thread = new Thread(() -> {
                    final Object app;
                    try {
                        app = runner.get();
                        startup.complete(app);
                    } catch (final Exception e) {
                        e.addSuppressed(new Throwable("Failing context is: " + ctx.ctxName));
                        startup.completeExceptionally(e);
                        shutdown.completeExceptionally(e);
                        return;
                    }
                    try {
                        shutdownStart.get();
                    } catch (final InterruptedException e) {
                        Thread.currentThread().interrupt();
                    } catch (final ExecutionException e) {
                    } finally {
                        shutdown.complete(null);
                    }
                }, "main:" + ctx.ctxName);
                threadCustomizer.accept(thread);
                thread.start();
                return startup;
            });
            return this;
        }

        public JsaCrash build(final Object testCase) {
            contextSuppliers.put(TEST_CASE_CONTEXT, () -> completedFuture(startTestCaseCtx()));
            builtRule = new JsaCrash(testCase, contextSuppliers, innerRules, shutdownStart, shutdownEnd);
            return builtRule;
        }

        private Object startTestCaseCtx() {
            return null;
        }

        public JsaCrash build() {
            return build(new Object());
        }

        public class ContextBuilder {
            private final String ctxName;
            private final Map<String, Object> properties = new HashMap<>(baseProperties);

            ContextBuilder(final String ctxName) {
                this.ctxName = ctxName;
            }
            public Builder as(final Class<?> configurationClass) {
                return addContext(this, configurationClass);
            }

        }
    }

    public interface ServiceSpec {
        default String groupId() {
            return "my.service";
        }
        String artifactId();
        String version();
        Optional<String> mainClass();

    }

    public interface ResolvedService {
        ServiceSpec spec();
        Path tarball();

        static ResolvedService of(final ServiceSpec spec, final Path tarball) {
            return new ResolvedService() {
                @Override
                public Path tarball() {
                    return tarball;
                }

                @Override
                public ServiceSpec spec() {
                    return spec;
                }
            };
        }
    }

    public static ServiceSpec service(final String artifactId, final String version) {
        return new ServiceSpec() {
            @Override
            public String version() {
                return version;
            }

            @Override
            public Optional<String> mainClass() {
                return Optional.empty();
            }

            @Override
            public String artifactId() {
                return artifactId;
            }
        };
    }
}
