package cz.moneta.test.harness.performance;

import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.support.util.Level;
import org.apache.commons.lang.exception.ExceptionUtils;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.nio.file.StandardOpenOption.CREATE_NEW;

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

    private static final String REPORT_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testResults version=\"1.2\">";
    private static final String REPORT_FOOTER = "</testResults>";

    private PerformanceWrapper() {
    }

    public static <I extends PerformanceAware> I wrap(I wrapped, String reportPath, StoreAccessor harness, Class<? super I>... interfaces) {
        return (I) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                interfaces,
                new PerformanceInvocationHandler(wrapped, reportPath, harness));
    }

    private static class PerformanceInvocationHandler implements InvocationHandler {

        private final PerformanceAware wrapped;
        private final List<PerformanceReportEntry> performanceEntries = new CopyOnWriteArrayList<>();
        private final Path path;

        public PerformanceInvocationHandler(PerformanceAware wrapped, String reportPath, StoreAccessor harness) {
            this.wrapped = wrapped;
            this.path = Stream.of(reportPath, "performance-report.jtl")
                    .filter(Objects::nonNull)
                    .findFirst()
                    .map(Paths::get)
                    .map(p -> {
                        try {
                            Files.deleteIfExists(p);
                        } catch (IOException e) {
                            harness.log(Level.ERROR, "Could not delete existing performance report:\n%s",
                                    ExceptionUtils.getFullStackTrace(e));
                            return null;
                        }
                        return p;
                    })
                    .orElseThrow(() -> new IllegalStateException("Could not create performance report file"));
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
            if ("publishReport".equals(method.getName())) {
                Stream.of(
                        Stream.of(REPORT_HEADER),
                        performanceEntries.stream().map(PerformanceReportEntry::asReportLine),
                        Stream.of(REPORT_FOOTER))
                        .flatMap(s -> s)
                        .collect(Collectors.collectingAndThen(Collectors.toList(), this::writeLines));

                return null;
            } else {
                long start = System.currentTimeMillis();
                String reportKey = wrapped.getPerformanceReportKey(proxy, method, args);
                boolean success = true;
                try {
                    return method.invoke(wrapped, args);
                } catch (Exception e) {
                    success = false;
                    throw e;
                } finally {
                    long end = System.currentTimeMillis();
                    performanceEntries.add(new PerformanceReportEntry(reportKey, start, end, success));
                }
            }
        }

        private Path writeLines(List<String> lines) {
            try {
                return Files.write(path, lines, CREATE_NEW);
            } catch (IOException e) {
                throw new IllegalStateException(String.format("Could not write performance report:%n%s",
                        lines.stream().collect(Collectors.joining("\n")), e));
            }
        }

    }
}
