package cz.moneta.test.harness.context;

import cz.moneta.test.harness.annotations.TestContext;
import cz.moneta.test.harness.endpoints.Endpoint;
import cz.moneta.test.harness.exception.HarnessConfigurationException;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.data.Generator;
import cz.moneta.test.harness.support.data.GeneratorType;
import cz.moneta.test.harness.support.util.Level;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.extension.ExtensionContext.Store;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static cz.moneta.test.harness.HarnessJunit5Extension.ACTIVE_ENDPOINTS;

@SuppressWarnings("unchecked")
@TestContext
public abstract class BaseStoreAccessor implements StoreAccessor {

    private static final Logger LOG = LogManager.getLogger("Harness");

    private final Store rootStore;
    private final Store globalStore;
    private final Store endpointStore;
    private final Store configStore;
    private final Store generatorsStore;

    public BaseStoreAccessor(Store rootStore, Store globalStore, Store endpointStore, Store configStore, Store generatorsStore) {
        this.rootStore = rootStore;
        this.globalStore = globalStore;
        this.endpointStore = endpointStore;
        this.configStore = configStore;
        this.generatorsStore = generatorsStore;
    }

    @Override
    public <E extends Endpoint> E getEndpoint(Class<E> endpointClass, Object... args) {
        Set<Pair<Class<E>, List<Object>>> activeEndpoints = endpointStore.get(ACTIVE_ENDPOINTS, Set.class);
        return endpointStore.getOrComputeIfAbsent(
                Pair.of(endpointClass, Arrays.asList(args)),
                c -> {
                    try {
                        Class[] paramTypes = Stream.concat(
                                Stream.of(StoreAccessor.class),
                                Arrays.stream(args).map(Object::getClass)).toArray(Class[]::new);
                        Constructor<E> constructor = endpointClass.getConstructor(paramTypes);
                        return constructor.newInstance(ArrayUtils.insert(0, args, this));
                    } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
                        throw Stream.of(Optional.ofNullable(e.getCause()), Optional.of(e))
                                .filter(Optional::isPresent)
                                .map(Optional::get)
                                .findFirst()
                                .map(t -> new IllegalStateException("Endpoint " + endpointClass.getSimpleName() +
                                        " could not be instantiated: " + t.getMessage(), e)
                                )
                                .get();
                    } finally {
                        activeEndpoints.add(c);
                    }
                },
                endpointClass);
    }

    @Override
    public void closeEndpoint(Endpoint activeEndpoint) {
        Set<Pair<Class<Endpoint>, List<Object>>> activeEndpoints = endpointStore.get(ACTIVE_ENDPOINTS, Set.class);
        Pair<Class<Endpoint>, List<Object>> pair = activeEndpoints.stream()
                .filter(p -> endpointStore.get(p, Endpoint.class) == activeEndpoint)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Cannot find active endpoint to close"));

        Endpoint endpoint = endpointStore.get(pair, Endpoint.class);
        endpoint.close();
        activeEndpoints.remove(pair);
        endpointStore.remove(pair, Endpoint.class);
    }

    @Override
    public <E extends Endpoint> Set<E> getActiveEndpoints() {
        Set<Pair<Class<E>, List<Object>>> activeEndpoints = endpointStore.get(ACTIVE_ENDPOINTS, Set.class);
        return activeEndpoints.stream()
                .map(pair -> (E) endpointStore.get(pair, Endpoint.class))
                .collect(Collectors.toSet());
    }

    @Override
    public String getConfig(String key) {
        return configStore.get(key, String.class);
    }

    @Override
    public String getConfig(String key, String defaultValue) {
        return Optional.ofNullable(configStore.get(key, String.class)).orElse(defaultValue);
    }

    @Override
    public void putConfig(String key, String value) {
        log(Level.INFO, "Value: " + value + " is stored with key: " + key);
        configStore.put(key, value);
    }

    @Override
    public <T> T get(String key) {
        return get(key, false);
    }

    @Override
    public <T> T get(String key, boolean nullEnabled) {
        if (nullEnabled) {
            return (T) globalStore.get(key);
        } else {
            return (T) Optional.ofNullable(globalStore.get(key))
                    .orElseThrow(() -> new HarnessException("Error getting value from store, key: " + key + " is null\nCheck if value in this key is correctly set"));
        }
    }

    @Override
    public void store(String key, Object value) {
        log(Level.INFO, "Value: " + value + " is stored with key: " + key);
        globalStore.put(key, value);
    }

    @Override
    public void storeGlobal(String key, Object value) {
        log(Level.INFO, "Value: " + value + " is stored with key: " + key);
        rootStore.put(key, value);
    }

    @Override
    public void storeGlobal(String key, Supplier orCreate) {
        log(Level.INFO, "Value: " + orCreate.get().toString() + " is stored with key: " + key);
        rootStore.getOrComputeIfAbsent(key, k -> orCreate.get());
    }

    @Override
    public <T> T getGlobal(String key, Supplier<T> orCreate) {
        return (T) rootStore.getOrComputeIfAbsent(key, k -> orCreate.get());
    }

    @Override
    public <T> T getGlobal(String key) {
        return (T) rootStore.get(key);
    }

    @Override
    public void log(String template, Object... args) {
        LOG.info(template, args);
    }

    @Override
    public void log(Level level, String template, Object... args) {
        LOG.log(org.apache.logging.log4j.Level.getLevel(level.name()), template, args);
    }

    protected void addGenerator(GeneratorType type, Generator<?> generator) {
        generatorsStore.put(type, generator);
    }

    @Override
    public <T> T generate(GeneratorType type, Object... params) {
        return (T) generatorsStore.get(type, Generator.class).generate(this, params);
    }

    /**
     * Get config value from system property or value from config store.
     * <p>
     *     Value get by priority:
     *     <ol>
     *         <li>
     *             System property (e.g. passed by maven -D)
     *         </li>
     *         <li>
     *             Value from config store
     *         </li>
     *     </ol>
     * </p>
     * <p>
     *     In case of value not found HarnessConfigurationException is thrown.
     * </p>
     *
     * @param       key identifier
     * @return      String value for defined key
     */
    public String getSystemOrConfigValue(String key) {
        return Stream.of(Optional.ofNullable(System.getProperty(key)), Optional.ofNullable(getConfig(key)))
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst()
                .orElseThrow(() -> new HarnessConfigurationException(("You need to configure " + key + " parameter!")));
    }
}

