/*
 * Copyright ConsenSys AG.
 *
 * Licensed 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package org.hyperledger.besu.ethereum.core;

import static java.nio.charset.StandardCharsets.UTF_8;

import org.hyperledger.besu.crypto.KeyPair;
import org.hyperledger.besu.crypto.KeyPairUtil;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.enclave.Enclave;
import org.hyperledger.besu.enclave.EnclaveFactory;
import org.hyperledger.besu.ethereum.privacy.PrivateStateGenesisAllocator;
import org.hyperledger.besu.ethereum.privacy.PrivateStateRootResolver;
import org.hyperledger.besu.ethereum.privacy.PrivateWorldStateReader;
import org.hyperledger.besu.ethereum.privacy.storage.PrivacyStorageProvider;
import org.hyperledger.besu.ethereum.privacy.storage.PrivateStateStorage;
import org.hyperledger.besu.ethereum.trie.forest.ForestWorldStateArchive;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.ethereum.worldstate.WorldStatePreimageStorage;
import org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator;
import org.hyperledger.besu.evm.internal.EvmConfiguration;
import org.hyperledger.besu.plugin.services.PrivacyPluginService;
import org.hyperledger.besu.plugin.services.privacy.PrivacyGroupGenesisProvider;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Collections;
import java.util.Optional;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.Files;

public class PrivacyParameters {

  // Last address that can be generated for a pre-compiled contract
  public static final Integer PRIVACY = Byte.MAX_VALUE - 1;
  public static final Address DEFAULT_PRIVACY = Address.precompiled(PRIVACY);
  public static final Address FLEXIBLE_PRIVACY = Address.precompiled(PRIVACY - 1);

  // Flexible privacy management contracts (injected in private state)
  public static final Address FLEXIBLE_PRIVACY_PROXY = Address.precompiled(PRIVACY - 2);
  public static final Address DEFAULT_FLEXIBLE_PRIVACY_MANAGEMENT =
      Address.precompiled(PRIVACY - 3);

  public static final Address PLUGIN_PRIVACY = Address.precompiled(PRIVACY - 4);

  public static final URI DEFAULT_ENCLAVE_URL = URI.create("http://localhost:8888");
  public static final PrivacyParameters DEFAULT = new PrivacyParameters();

  private boolean enabled;
  private URI enclaveUri;
  private String privacyUserId;
  private File enclavePublicKeyFile;
  private Optional<KeyPair> signingKeyPair = Optional.empty();
  private Enclave enclave;
  private PrivacyStorageProvider privateStorageProvider;
  private WorldStateArchive privateWorldStateArchive;
  private PrivateStateStorage privateStateStorage;
  private boolean multiTenancyEnabled;
  private boolean flexiblePrivacyGroupsEnabled;
  private boolean privacyPluginEnabled;
  private PrivateStateRootResolver privateStateRootResolver;
  private PrivateWorldStateReader privateWorldStateReader;
  private PrivacyPluginService privacyPluginService;
  private boolean privateNonceAlwaysIncrementsEnabled;

  public Address getPrivacyAddress() {
    if (isPrivacyPluginEnabled()) {
      return PLUGIN_PRIVACY;
    } else if (isFlexiblePrivacyGroupsEnabled()) {
      return FLEXIBLE_PRIVACY;
    } else {
      return DEFAULT_PRIVACY;
    }
  }

  public Boolean isEnabled() {
    return enabled;
  }

  private void setEnabled(final boolean enabled) {
    this.enabled = enabled;
  }

  public URI getEnclaveUri() {
    return enclaveUri;
  }

  private void setEnclaveUri(final URI enclaveUri) {
    this.enclaveUri = enclaveUri;
  }

  public String getPrivacyUserId() {
    return privacyUserId;
  }

  @VisibleForTesting
  public void setPrivacyUserId(final String privacyUserId) {
    this.privacyUserId = privacyUserId;
  }

  public File getEnclavePublicKeyFile() {
    return enclavePublicKeyFile;
  }

  private void setEnclavePublicKeyFile(final File enclavePublicKeyFile) {
    this.enclavePublicKeyFile = enclavePublicKeyFile;
  }

  public Optional<KeyPair> getSigningKeyPair() {
    return signingKeyPair;
  }

  private void setSigningKeyPair(final KeyPair signingKeyPair) {
    this.signingKeyPair = Optional.ofNullable(signingKeyPair);
  }

  public WorldStateArchive getPrivateWorldStateArchive() {
    return privateWorldStateArchive;
  }

  private void setPrivateWorldStateArchive(final WorldStateArchive privateWorldStateArchive) {
    this.privateWorldStateArchive = privateWorldStateArchive;
  }

  public PrivacyStorageProvider getPrivateStorageProvider() {
    return privateStorageProvider;
  }

  private void setPrivateStorageProvider(final PrivacyStorageProvider privateStorageProvider) {
    this.privateStorageProvider = privateStorageProvider;
  }

  public PrivateStateStorage getPrivateStateStorage() {
    return privateStateStorage;
  }

  private void setPrivateStateStorage(final PrivateStateStorage privateStateStorage) {
    this.privateStateStorage = privateStateStorage;
  }

  public Enclave getEnclave() {
    return enclave;
  }

  private void setEnclave(final Enclave enclave) {
    this.enclave = enclave;
  }

  private void setMultiTenancyEnabled(final boolean multiTenancyEnabled) {
    this.multiTenancyEnabled = multiTenancyEnabled;
  }

  public boolean isMultiTenancyEnabled() {
    return multiTenancyEnabled;
  }

  private void setFlexiblePrivacyGroupsEnabled(final boolean flexiblePrivacyGroupsEnabled) {
    this.flexiblePrivacyGroupsEnabled = flexiblePrivacyGroupsEnabled;
  }

  public boolean isFlexiblePrivacyGroupsEnabled() {
    return flexiblePrivacyGroupsEnabled;
  }

  private void setPrivacyPluginEnabled(final boolean privacyPluginEnabled) {
    this.privacyPluginEnabled = privacyPluginEnabled;
  }

  public boolean isPrivacyPluginEnabled() {
    return privacyPluginEnabled;
  }

  public PrivateStateRootResolver getPrivateStateRootResolver() {
    return privateStateRootResolver;
  }

  private void setPrivateStateRootResolver(
      final PrivateStateRootResolver privateStateRootResolver) {
    this.privateStateRootResolver = privateStateRootResolver;
  }

  public PrivateWorldStateReader getPrivateWorldStateReader() {
    return privateWorldStateReader;
  }

  private void setPrivateWorldStateReader(final PrivateWorldStateReader privateWorldStateReader) {
    this.privateWorldStateReader = privateWorldStateReader;
  }

  private void setPrivacyService(final PrivacyPluginService privacyPluginService) {
    this.privacyPluginService = privacyPluginService;
  }

  public PrivacyPluginService getPrivacyService() {
    return privacyPluginService;
  }

  public PrivateStateGenesisAllocator getPrivateStateGenesisAllocator() {
    // Note: the order of plugin registration may cause issues here.
    // This is why it's instantiated on get. It's needed in the privacy pre-compile constructors
    // but privacy parameters is built before the plugin has had a chance to register a provider
    // and have cli options instantiated
    return new PrivateStateGenesisAllocator(
        flexiblePrivacyGroupsEnabled, createPrivateGenesisProvider());
  }

  private PrivacyGroupGenesisProvider createPrivateGenesisProvider() {
    if (privacyPluginService != null
        && privacyPluginService.getPrivacyGroupGenesisProvider() != null) {
      return privacyPluginService.getPrivacyGroupGenesisProvider();
    } else {
      return (privacyGroupId, blockNumber) -> Collections::emptyList;
    }
  }

  public boolean isPrivateNonceAlwaysIncrementsEnabled() {
    return privateNonceAlwaysIncrementsEnabled;
  }

  public void setPrivateNonceAlwaysIncrementsEnabled(
      final boolean privateNonceAlwaysIncrementsEnabled) {
    this.privateNonceAlwaysIncrementsEnabled = privateNonceAlwaysIncrementsEnabled;
  }

  @Override
  public String toString() {
    return "PrivacyParameters{"
        + "enabled="
        + enabled
        + ", multiTenancyEnabled = "
        + multiTenancyEnabled
        + ", privacyPluginEnabled = "
        + privacyPluginEnabled
        + ", flexiblePrivacyGroupsEnabled = "
        + flexiblePrivacyGroupsEnabled
        + ", enclaveUri='"
        + enclaveUri
        + ", privatePayloadEncryptionService='"
        + privacyPluginService.getClass().getSimpleName()
        + '\''
        + '}';
  }

  public static class Builder {

    private boolean enabled;
    private URI enclaveUrl;
    private File enclavePublicKeyFile;
    private String privacyUserId;
    private Path privateKeyPath;
    private PrivacyStorageProvider storageProvider;
    private EnclaveFactory enclaveFactory;
    private boolean multiTenancyEnabled;
    private Path privacyKeyStoreFile;
    private Path privacyKeyStorePasswordFile;
    private Path privacyTlsKnownEnclaveFile;
    private boolean flexiblePrivacyGroupsEnabled;
    private boolean privacyPluginEnabled;
    private PrivacyPluginService privacyPluginService;
    private boolean privateNonceAlwaysIncrementsEnabled;

    public Builder setEnclaveUrl(final URI enclaveUrl) {
      this.enclaveUrl = enclaveUrl;
      return this;
    }

    public Builder setEnabled(final boolean enabled) {
      this.enabled = enabled;
      return this;
    }

    public Builder setStorageProvider(final PrivacyStorageProvider privateStorageProvider) {
      this.storageProvider = privateStorageProvider;
      return this;
    }

    public Builder setPrivateKeyPath(final Path privateKeyPath) {
      this.privateKeyPath = privateKeyPath;
      return this;
    }

    public Builder setEnclaveFactory(final EnclaveFactory enclaveFactory) {
      this.enclaveFactory = enclaveFactory;
      return this;
    }

    public Builder setMultiTenancyEnabled(final boolean multiTenancyEnabled) {
      this.multiTenancyEnabled = multiTenancyEnabled;
      return this;
    }

    public Builder setPrivacyKeyStoreFile(final Path privacyKeyStoreFile) {
      this.privacyKeyStoreFile = privacyKeyStoreFile;
      return this;
    }

    public Builder setPrivacyKeyStorePasswordFile(final Path privacyKeyStorePasswordFile) {
      this.privacyKeyStorePasswordFile = privacyKeyStorePasswordFile;
      return this;
    }

    public Builder setPrivacyTlsKnownEnclaveFile(final Path privacyTlsKnownEnclaveFile) {
      this.privacyTlsKnownEnclaveFile = privacyTlsKnownEnclaveFile;
      return this;
    }

    public Builder setFlexiblePrivacyGroupsEnabled(final boolean flexiblePrivacyGroupsEnabled) {
      this.flexiblePrivacyGroupsEnabled = flexiblePrivacyGroupsEnabled;
      return this;
    }

    public Builder setPrivateNonceAlwaysIncrementsEnabled(
        final boolean isPrivateNonceAlwaysIncrementsEnabled) {
      this.privateNonceAlwaysIncrementsEnabled = isPrivateNonceAlwaysIncrementsEnabled;
      return this;
    }

    public Builder setPrivacyPluginEnabled(final boolean privacyPluginEnabled) {
      this.privacyPluginEnabled = privacyPluginEnabled;
      return this;
    }

    public Builder setPrivacyUserIdUsingFile(final File publicKeyFile) throws IOException {
      this.enclavePublicKeyFile = publicKeyFile;
      this.privacyUserId = Files.asCharSource(publicKeyFile, UTF_8).read();
      // throws exception if invalid base 64
      Base64.getDecoder().decode(this.privacyUserId);
      return this;
    }

    public Builder setPrivacyService(final PrivacyPluginService privacyPluginService) {
      this.privacyPluginService = privacyPluginService;
      return this;
    }

    public PrivacyParameters build() {
      final PrivacyParameters config = new PrivacyParameters();
      if (enabled) {
        final WorldStateStorageCoordinator privateWorldStateStorage =
            storageProvider.createWorldStateStorageCoordinator();
        final WorldStatePreimageStorage privatePreimageStorage =
            storageProvider.createWorldStatePreimageStorage();
        final WorldStateArchive privateWorldStateArchive =
            new ForestWorldStateArchive(
                privateWorldStateStorage, privatePreimageStorage, EvmConfiguration.DEFAULT);

        final PrivateStateStorage privateStateStorage = storageProvider.createPrivateStateStorage();
        final PrivateStateRootResolver privateStateRootResolver =
            new PrivateStateRootResolver(privateStateStorage);

        config.setPrivateStateRootResolver(privateStateRootResolver);
        config.setPrivateWorldStateReader(
            new PrivateWorldStateReader(
                privateStateRootResolver, privateWorldStateArchive, privateStateStorage));

        config.setPrivateWorldStateArchive(privateWorldStateArchive);

        config.setPrivacyService(privacyPluginService);

        config.setPrivateStorageProvider(storageProvider);
        config.setPrivateStateStorage(privateStateStorage);

        config.setPrivacyUserId(privacyUserId);
        config.setEnclavePublicKeyFile(enclavePublicKeyFile);
        // pass TLS options to enclave factory if they are set
        if (privacyKeyStoreFile != null) {
          config.setEnclave(
              enclaveFactory.createVertxEnclave(
                  enclaveUrl,
                  privacyKeyStoreFile,
                  privacyKeyStorePasswordFile,
                  privacyTlsKnownEnclaveFile));
        } else {
          config.setEnclave(enclaveFactory.createVertxEnclave(enclaveUrl));
        }
        config.setEnclaveUri(enclaveUrl);

        if (privateKeyPath != null) {
          config.setSigningKeyPair(KeyPairUtil.load(privateKeyPath.toFile()));
        }
      }
      config.setEnabled(enabled);
      config.setMultiTenancyEnabled(multiTenancyEnabled);
      config.setFlexiblePrivacyGroupsEnabled(flexiblePrivacyGroupsEnabled);
      config.setPrivacyPluginEnabled(privacyPluginEnabled);
      config.setPrivateNonceAlwaysIncrementsEnabled(privateNonceAlwaysIncrementsEnabled);
      return config;
    }
  }
}
