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

import cz.moneta.test.harness.connectors.VaultConnector;
import cz.moneta.test.harness.constants.HarnessConfigConstants;
import cz.moneta.test.harness.context.StoreAccessor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;

public final class AuthSupport {

    private static final String USERNAME_CREDENTIALS = "credentials.%s.username";
    private static final String PASSWORD_CREDENTIALS = "credentials.%s.password";
    private static final String TOKEN_CREDENTIALS = "credentials.%s.token";
    private static final String PROPERTIES_FILE_SOURCE = "properties file";
    private static final String VAULT_SOURCE = "Vault";

    private static final Logger logger = LogManager.getLogger(AuthSupport.class);

    private AuthSupport() {
    }

    public static Credentials getCredentials(String path, StoreAccessor store) {
        return getCredentialsOptional(path, store)
                .orElseThrow(() -> new IllegalStateException(String.format("Credentials not found either through " +
                        "vault nor in properties. Please define 'credentials.%1$s.username' " +
                        "and 'credentials.%1$s.password' properties in order to use local configuration.", path)));
    }

    public static boolean hasCredentials(String path, StoreAccessor store) {
        return getCredentialsOptional(path, store).isPresent();
    }

    private static Optional<Credentials> getCredentialsOptional(String path, StoreAccessor store) {
        return Stream.<Supplier<Optional<Credentials>>>of(
                () -> getCredentialsFromProperties(store, path),
                () -> getCredentialsFromVault(store, path))
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst();
    }

    private static Optional<Credentials> getCredentialsFromProperties(StoreAccessor store, String path) {
        Optional<Credentials> credentialsFromProperties = Optional
                .ofNullable(getConfigFromProperties(USERNAME_CREDENTIALS, store, path))
                .flatMap(u -> Optional.ofNullable(getConfigFromProperties(PASSWORD_CREDENTIALS, store, path))
                        .map(p -> new Credentials(u, p)));

        logUsernameAndSource(PROPERTIES_FILE_SOURCE, credentialsFromProperties);
        return credentialsFromProperties;
    }

    private static String getConfigFromProperties(String identifier, StoreAccessor store, String path) {
        return store.getConfig(String.format(identifier, path));
    }

    private static Optional<Credentials> getCredentialsFromVault(StoreAccessor store, String path) {
        Optional<Credentials> credentialsFromVault = Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_URL_CONFIG))
                .flatMap(u -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_USERNAME_CONFIG))
                .flatMap(l -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_PASSWORD_CONFIG))
                                .map(p -> new VaultConnector(u, l, p))))
                .flatMap(v -> Optional.of("vault.path.base")
                        .map(store::getConfig)
                        .flatMap(p -> v.getUsernameAndPassword(p + "/" + path)));

        logUsernameAndSource(VAULT_SOURCE, credentialsFromVault);
        return credentialsFromVault;
    }

    private static void logUsernameAndSource(String usernameSource, Optional<Credentials> credentialsSource) {
        Credentials credentials = Stream.of(credentialsSource)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst()
                .orElse(null);
        if (credentials != null) {
            logger.info("Username source: {}.", () -> usernameSource);
            logger.info("Used username: {}.", credentials::getUsername);
        }
    }

    public static String getAccessToken(String path, StoreAccessor store) {
        return Stream.<Supplier<Optional<String>>>of(
                () -> getAccessTokenFromConfig(String.format(TOKEN_CREDENTIALS, path), store),
                () -> getAccessTokenFromVault(path, store))
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst()
                .orElseThrow(() -> new IllegalStateException(String.format("Token not found either through " +
                        "vault nor in properties. Please define '" + TOKEN_CREDENTIALS + "' properties in order to use local configuration.", path)));
    }

    private static Optional<String> getAccessTokenFromConfig(String path, StoreAccessor store) {
        return Optional.ofNullable(store.getConfig(path));
    }

    private static Optional<String> getAccessTokenFromVault(String path, StoreAccessor store) {
        return Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_URL_CONFIG))
                .flatMap(u -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_USERNAME_CONFIG))
                        .flatMap(l -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_PASSWORD_CONFIG))
                                .map(p -> new VaultConnector(u, l, p))))
                .flatMap(v -> Optional.of("vault.path.base")
                        .map(store::getConfig)
                        .flatMap(p -> v.getValue(p + "/" + path, "token")));
    }

    public static String getClientSecret(String clientKey, StoreAccessor store, String keysPath) {
        return Stream.of(store.getConfig("vault.password"))
                .filter(pwd -> pwd != null && !pwd.isEmpty())
                .map(w -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_URL_CONFIG))
                        .flatMap(u -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_USERNAME_CONFIG))
                                .flatMap(l -> Optional.ofNullable(store.getConfig(HarnessConfigConstants.VAULT_PASSWORD_CONFIG))
                                        .map(x -> new VaultConnector(u, l, w))))
                        .flatMap(v -> Optional.of(keysPath)
                                .map(store::getConfig)
                                .flatMap(p -> v.getValue(p, clientKey))
                                .map(String::valueOf))
                        .orElseThrow(() -> new IllegalStateException("Client secret '" + clientKey + "' could not be read from Vault")))
                .findFirst()
                .orElseGet(() -> Optional.ofNullable(store.getConfig(clientKey))
                        .map(String::valueOf)
                        .orElseThrow(() -> new IllegalStateException("Client secret '" + clientKey + "' could not be read from Vault or local config file")));
    }
}
