package cz.moneta.test.harness.connectors;

import com.bettercloud.vault.SslConfig;
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
import com.bettercloud.vault.api.Logical;
import com.bettercloud.vault.response.LogicalResponse;
import cz.moneta.test.harness.connectors.rest.BaseRestConnector;
import cz.moneta.test.harness.support.auth.Credentials;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.security.KeyStore;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class VaultConnector extends BaseRestConnector {

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

    private static String url;
    private static String username;
    private static String pwd;

    private static Vault vault;
    private static VaultConfig config;

    private static LocalDateTime tokenExpiration;

    public VaultConnector(String url, String username, String pwd) {
        this.url = url;
        this.username = username;
        this.pwd = pwd;

        if (tokenExpiration == null) {
           initVaultConnection();
        } else if (tokenExpiration.minusMinutes(1).isBefore(LocalDateTime.now())) {
           loginVault();
        }
    }

    private void initVaultConnection() {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(this.getClass().getClassLoader().getResourceAsStream("keystores/mb_root"), "changeit".toCharArray());
            config = new VaultConfig()
                    .address(url)
                    .sslConfig(new SslConfig()
                            .trustStore(keyStore)
                            .build())
                    .build();

            vault = new Vault(config, 2);

            loginVault();

        } catch (Exception e) {
            throw new IllegalStateException("Failed to connect to vault url: " + url, e);
        }
    }

    private void loginVault() {
        try {
            String token = vault.auth().loginByUserPass(username, pwd).getAuthClientToken();
            config.token(token).build();

            long ttl = vault.auth().lookupSelf().getTTL();
            tokenExpiration = LocalDateTime.now().plusSeconds(ttl);
        } catch (VaultException e) {
            LOG.error("Cannot get Vault token", e);
        }
    }

    public Optional<Credentials> getUsernameAndPassword(String path) {
        return Optional.ofNullable(vault.logical())
                .map(v -> readValue(path, v))
                .map(LogicalResponse::getDataObject)
                .flatMap(d -> Optional.ofNullable(d.getString("username")).map(usr -> Pair.of(usr, d)))
                .flatMap(p -> Optional.ofNullable(p.getRight().getString("password")).map(pwd -> new Credentials(p.getLeft(), pwd)));
    }

    @SuppressWarnings("unchecked")
    public <T> Optional<T> getValue(String path, String key) {
        return Optional.ofNullable(vault.logical())
                .map(v -> readValue(path, v))
                .map(LogicalResponse::getDataObject)
                .map(d -> (T) d.getString(key));
    }

    public void setKeyValue(String path, String key, String value) {
        Optional.ofNullable(readValue(path, vault.logical()))
                .map(LogicalResponse::getData)
                .map(map -> new HashMap<String, Object>(map))
                .map(m -> {
                    m.put(key, value);
                    return m;
                })
                .map(m -> writeValue(path, m, vault.logical()))
                .orElseThrow(() -> new IllegalStateException("Cannot write value to vault"));
    }

    private LogicalResponse writeValue(String path, Map<String, Object> value, Logical vault) {
        try {
            return vault.write(path, value);
        } catch (VaultException e) {
            LOG.error("Failed to write value from vault: {}\n{}", path, e);
            return null;
        }
    }

    private LogicalResponse readValue(String path, Logical v) {
        try {
            return v.read(path);
        } catch (VaultException e) {
            LOG.error("Failed to read value from vault: {}\n{}", path, e);
            return null;
        }
    }
}
