// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.athenz.instanceproviderservice;

import com.google.inject.Inject;
import com.yahoo.component.AbstractComponent;
import com.yahoo.config.provision.Zone;
import com.yahoo.jdisc.http.ssl.SslKeyStoreConfigurator;
import com.yahoo.jdisc.http.ssl.SslKeyStoreContext;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.AthenzCertificateClient;

import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;

import static com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils.getZoneConfig;

/**
 * @author bjorncs
 */
// TODO Cache certificate on disk
@SuppressWarnings("unused") // Component injected into Jetty connector factory
public class AthenzSslKeyStoreConfigurator extends AbstractComponent implements SslKeyStoreConfigurator {
    private static final Logger log = Logger.getLogger(AthenzSslKeyStoreConfigurator.class.getName());
    // TODO Make expiry and update frequency configurable parameters
    private static final Duration CERTIFICATE_EXPIRY_TIME = Duration.ofDays(30);
    private static final Duration CERTIFICATE_UPDATE_PERIOD = Duration.ofDays(7);
    private static final String DUMMY_PASSWORD = "athenz";

    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private final AthenzCertificateClient certificateClient;
    private final KeyProvider keyProvider;
    private final AthenzProviderServiceConfig.Zones zoneConfig;
    private final AtomicBoolean alreadyConfigured = new AtomicBoolean();
    private final AtomicReference<KeyStore> initialKeyStore = new AtomicReference<>();

    @Inject
    public AthenzSslKeyStoreConfigurator(KeyProvider keyProvider,
                                         AthenzProviderServiceConfig config,
                                         Zone zone) {
        AthenzProviderServiceConfig.Zones zoneConfig = getZoneConfig(config, zone);
        this.certificateClient = new AthenzCertificateClient(config, zoneConfig);
        this.keyProvider = keyProvider;
        this.zoneConfig = zoneConfig;
        this.initialKeyStore.set(downloadCertificate(keyProvider, certificateClient, zoneConfig));
    }

    @Override
    public void configure(SslKeyStoreContext sslKeyStoreContext) {
        if (alreadyConfigured.getAndSet(true)) { // For debugging purpose of SslKeyStoreConfigurator interface
            throw new IllegalStateException("Already configured. configure() can only be called once.");
        }
        sslKeyStoreContext.updateKeyStore(initialKeyStore.getAndSet(null), DUMMY_PASSWORD);
        scheduler.scheduleAtFixedRate(new AthenzCertificateUpdater(sslKeyStoreContext),
                                      CERTIFICATE_UPDATE_PERIOD.toMinutes()/*initial delay*/,
                                      CERTIFICATE_UPDATE_PERIOD.toMinutes(),
                                      TimeUnit.MINUTES);
    }

    @Override
    public void deconstruct() {
        try {
            scheduler.shutdownNow();
            scheduler.awaitTermination(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException("Failed to shutdown Athenz certificate updater on time", e);
        }
    }

    private static KeyStore downloadCertificate(KeyProvider keyProvider,
                                                AthenzCertificateClient certificateClient,
                                                AthenzProviderServiceConfig.Zones zoneConfig) {
        try {
            PrivateKey privateKey = keyProvider.getPrivateKey(zoneConfig.secretVersion());
            X509Certificate certificate = certificateClient.updateCertificate(privateKey, CERTIFICATE_EXPIRY_TIME);
            verifyActualExpiry(certificate);

            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(null);
            keyStore.setKeyEntry("athenz", privateKey, DUMMY_PASSWORD.toCharArray(), new Certificate[]{certificate});
            return keyStore;
        } catch (IOException | NoSuchAlgorithmException | CertificateException | KeyStoreException e) {
            throw new RuntimeException(e);
        }
    }

    private static void verifyActualExpiry(X509Certificate certificate) {
        Duration actualExpiry =
                Duration.between(certificate.getNotBefore().toInstant(),  certificate.getNotAfter().toInstant());
        if (CERTIFICATE_EXPIRY_TIME.compareTo(actualExpiry) > 0) {
            log.log(LogLevel.WARNING,
                    String.format("Expected expiry %s, got %s", CERTIFICATE_EXPIRY_TIME, actualExpiry));
        }
    }

    private class AthenzCertificateUpdater implements Runnable {

        private final SslKeyStoreContext sslKeyStoreContext;

        AthenzCertificateUpdater(SslKeyStoreContext sslKeyStoreContext) {
            this.sslKeyStoreContext = sslKeyStoreContext;
        }

        @Override
        public void run() {
            try {
                log.log(LogLevel.INFO, "Updating Athenz certificate from ZTS");
                KeyStore keyStore = downloadCertificate(keyProvider, certificateClient, zoneConfig);
                sslKeyStoreContext.updateKeyStore(keyStore, DUMMY_PASSWORD);
                log.log(LogLevel.INFO, "Athenz certificate reload successfully completed");
            } catch (Throwable e) {
                log.log(LogLevel.ERROR, "Failed to update certificate from ZTS: " + e.getMessage(), e);
            }
        }

    }
}

@@ -1145,60 +1145,8 @@
 an;%0A
-import java.util.concurrent.atomic.AtomicReference;%0A
 impo
@@ -2269,38 +2269,16 @@
 ate 
-final AtomicReference%3C
 KeyStore
 %3E in
@@ -2273,17 +2273,16 @@
 KeyStore
-%3E
  initial
@@ -2293,34 +2293,8 @@
 tore
- = new AtomicReference%3C%3E()
 ;%0A%0A 
@@ -2777,13 +2777,11 @@
 tore
-.set(
+ = 
 down
@@ -2839,17 +2839,16 @@
 eConfig)
-)
 ;%0A    %7D%0A
@@ -3216,24 +3216,8 @@
 tore
-.getAndSet(null)
 , DU
@@ -3223,32 +3223,64 @@
 UMMY_PASSWORD);%0A
+        initialKeyStore = null;%0A
         schedule

Remove unnecessary use of AtomicReference