package com.airlock.iam.sensitivevalues.application.service;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;
import java.security.KeyStore.PasswordProtection;
import java.security.KeyStore.SecretKeyEntry;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public class JceksSensitiveValuesProvider implements SensitiveValuesProvider {
   private static final String JCEKS_PATH = "jceks-keystore-path";
   private static final String JCEKS_PASSWORD = "jceks-keystore-password";
   private static final String ENCODED_EMPTY_STRING = "==empty";
   private File keystoreFile;
   private char[] storePassword;

   @Override
   public void initialize(FileLocationResolver fileLocationResolver, Properties properties) {
      this.requireProperty(properties, "jceks-keystore-path");
      this.requireProperty(properties, "jceks-keystore-password");
      this.keystoreFile = fileLocationResolver.resolveFileLocation(properties.getProperty("jceks-keystore-path"));
      if (this.keystoreFile.exists() && this.keystoreFile.canRead()) {
         this.storePassword = properties.getProperty("jceks-keystore-password").toCharArray();
      } else {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException(
            "Keystore file " + this.keystoreFile.getAbsolutePath() + " does not exist or is not readable"
         );
      }
   }

   private void requireProperty(Properties properties, String key) {
      if (properties.get(key) == null) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException("Properties must contain key \"" + key + "\"");
      }
   }

   @Override
   public void setSensitiveValue(String key, String value) {
      Objects.requireNonNull(value, "Value cannot be set to null");

      try {
         KeyStore jceks = this.loadKeyStore();
         jceks.setEntry(key, new SecretKeyEntry(this.valueToSecretKey(value)), new PasswordProtection(this.storePassword));
         this.saveKeyStore(jceks);
      } catch (KeyStoreException e) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException(e);
      }
   }

   @Override
   public void deleteSensitiveValue(String key) {
      try {
         KeyStore jceks = this.loadKeyStore();
         jceks.deleteEntry(key);
         this.saveKeyStore(jceks);
      } catch (KeyStoreException e) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException(e);
      }
   }

   @Override
   public List<String> getAllDefinedKeys() {
      try {
         KeyStore jceks = this.loadKeyStore();
         return Collections.list(jceks.aliases());
      } catch (KeyStoreException e) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException(e);
      }
   }

   @Override
   public Optional<String> getSensitiveValue(String key) {
      KeyStore jceks = this.loadKeyStore();

      try {
         SecretKeyEntry ske = (SecretKeyEntry)jceks.getEntry(key, new PasswordProtection(this.storePassword));
         return ske == null ? Optional.empty() : Optional.of(this.secretKeyToValue(ske.getSecretKey()));
      } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableEntryException e) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException(e);
      }
   }

   private SecretKey valueToSecretKey(String value) {
      String base64encoded;
      if (value.isEmpty()) {
         base64encoded = "==empty";
      } else {
         base64encoded = Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8));
      }

      try {
         SecretKeyFactory factory = SecretKeyFactory.getInstance("PBE");
         return factory.generateSecret(new PBEKeySpec(base64encoded.toCharArray()));
      } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException(e);
      }
   }

   private String secretKeyToValue(SecretKey secretKey) {
      try {
         SecretKeyFactory factory = SecretKeyFactory.getInstance("PBE");
         PBEKeySpec retrievedPassword = (PBEKeySpec)factory.getKeySpec(secretKey, PBEKeySpec.class);
         String base64Secret = new String(retrievedPassword.getPassword());
         return base64Secret.equals("==empty")
            ? ""
            : new String(Base64.getDecoder().decode(base64Secret.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
      } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException(e);
      }
   }

   private KeyStore loadKeyStore() {
      if (this.keystoreFile == null) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException("JceksSensitiveValuesProvider is not initialized");
      }

      try {
         KeyStore keystore = KeyStore.getInstance("JCEKS");
         if (this.keystoreFile.length() == 0L) {
            keystore.load(null, this.storePassword);
         } else {
            try (InputStream in = Files.newInputStream(this.keystoreFile.toPath())) {
               keystore.load(in, this.storePassword);
            }
         }

         return keystore;
      } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException(e);
      }
   }

   private void saveKeyStore(KeyStore keystore) {
      try (OutputStream out = Files.newOutputStream(this.keystoreFile.toPath())) {
         keystore.store(out, this.storePassword);
      } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
         throw new SensitiveValuesProvider.SensitiveValuesProviderException(e);
      }
   }
}
