/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.knox.gateway.backend.hashicorp.vault;

import org.apache.knox.gateway.backend.hashicorp.vault.authentication.HashicorpVaultClientAuthenticationProvider;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.AliasServiceException;
import org.apache.knox.gateway.util.PasswordUtils;
import org.springframework.vault.VaultException;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.core.VaultVersionedKeyValueOperations;
import org.springframework.vault.support.Versioned;

import java.net.URI;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;

public class HashicorpVaultAliasService implements AliasService {
  public static final String TYPE = "hashicorp.vault";
  public static final String VAULT_CONFIG_PREFIX = "hashicorp.vault.";
  public static final String VAULT_ADDRESS_KEY = VAULT_CONFIG_PREFIX + "address";

  private static final String KEY = "data";

  static final String VAULT_SEPARATOR = "/";
  static final String VAULT_SECRETS_ENGINE_KEY = VAULT_CONFIG_PREFIX + "secrets.engine";
  static final String VAULT_PATH_PREFIX_KEY = VAULT_CONFIG_PREFIX + "path.prefix";

  private final AliasService localAliasService;

  private VaultVersionedKeyValueOperations vault;
  private String vaultPathPrefix;
  private GatewayConfig config;

  public HashicorpVaultAliasService(AliasService localAliasService) {
    this.localAliasService = localAliasService;
  }

  private String getPath(String clusterName) {
    return vaultPathPrefix + clusterName;
  }

  private String getPath(String clusterName, String alias) {
    return getPath(clusterName) + VAULT_SEPARATOR + alias;
  }

  @Override
  public List<String> getAliasesForCluster(String clusterName) throws AliasServiceException {
    try {
      List<String> aliases = vault.list(getPath(clusterName));
      if(aliases == null) {
        return Collections.emptyList();
      }

      // Required to check if list entries are valid since versioned KV does a soft delete
      // Keys are still listed but do not have a value
      for (Iterator<String> iterator = aliases.iterator(); iterator.hasNext();) {
        String alias = iterator.next();
        if (getPasswordFromAliasForCluster(clusterName, alias) == null) {
          // Remove the current element from the iterator and the list.
          iterator.remove();
        }
      }

      return aliases;
    } catch (VaultException e) {
      throw new AliasServiceException(e);
    }
  }

  @Override
  public void addAliasForCluster(String clusterName, String alias, String value) throws AliasServiceException {
    try {
      vault.put(getPath(clusterName, alias), Collections.singletonMap(KEY, value));
    } catch (VaultException e) {
      throw new AliasServiceException(e);
    }
  }

  @Override
  public void addAliasesForCluster(String clusterName, Map<String, String> credentials) throws AliasServiceException {
    for (Map.Entry<String, String> credential : credentials.entrySet()) {
      addAliasForCluster(clusterName, credential.getKey(), credential.getValue());
    }
  }

  @Override
  public void removeAliasForCluster(String clusterName, String alias) throws AliasServiceException {
    // Delete is by default a soft delete with versioned KV in Vault
    // https://learn.hashicorp.com/vault/secrets-management/sm-versioned-kv#step-6-permanently-delete-data
    // Below is an example of how to programmatically delete all versions
    /*
    vaultTemplate.doWithSession(restOperations -> {
      restOperations.delete(VAULT_SEPARATOR + vaultSecretsEngine + "/metadata/" + clusterName + VAULT_SEPARATOR + alias);
      return null;
    });
     */
    try {
      vault.delete(getPath(clusterName, alias));
    } catch (VaultException e) {
      throw new AliasServiceException(e);
    }
  }

  @Override
  public void removeAliasesForCluster(String clusterName, Set<String> aliases) throws AliasServiceException {
    for (String alias : aliases) {
      removeAliasForCluster(clusterName, alias);
    }
  }

  @Override
  public char[] getPasswordFromAliasForCluster(String clusterName, String alias) throws AliasServiceException {
    try {
      Versioned<Map<String, Object>> mapVersioned = vault.get(getPath(clusterName, alias));
      if(mapVersioned != null && mapVersioned.hasData()) {
        Map<String, Object> data = mapVersioned.getData();
        if(data != null && data.containsKey(KEY)) {
          return String.valueOf(data.get(KEY)).toCharArray();
        }
      }
      return null;
    } catch (VaultException e) {
      throw new AliasServiceException(e);
    }
  }

  @Override
  public char[] getPasswordFromAliasForCluster(String clusterName, String alias, boolean generate) throws AliasServiceException {
    if(generate) {
      getPasswordFromAliasForCluster(clusterName, alias);
    }
    return getPasswordFromAliasForCluster(clusterName, alias);
  }

  @Override
  public void generateAliasForCluster(String clusterName, String alias) throws AliasServiceException {
    addAliasForCluster(clusterName, alias, PasswordUtils.generatePassword(16));
  }

  @Override
  public char[] getPasswordFromAliasForGateway(String alias) throws AliasServiceException {
    return getPasswordFromAliasForCluster(NO_CLUSTER_NAME, alias);
  }

  @Override
  public char[] getGatewayIdentityPassphrase() throws AliasServiceException {
    return getPasswordFromAliasForGateway(config.getIdentityKeyPassphraseAlias());
  }

  @Override
  public char[] getGatewayIdentityKeystorePassword() throws AliasServiceException {
    return getPasswordFromAliasForGateway(config.getIdentityKeystorePasswordAlias());
  }

  @Override
  public char[] getSigningKeyPassphrase() throws AliasServiceException {
    return getPasswordFromAliasForGateway(config.getSigningKeyPassphraseAlias());
  }

  @Override
  public char[] getSigningKeystorePassword() thr