/*
 * ******************************************************
 * Copyright 2019-2023 VMware, Inc.  All Rights Reserved.
 * ******************************************************
 */

package com.vmware.vsan.samples;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import jakarta.xml.ws.BindingProvider;

import java.security.MessageDigest;
import jakarta.xml.bind.DatatypeConverter;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.vmware.common.annotations.Action;
import com.vmware.common.annotations.Option;
import com.vmware.common.annotations.Sample;
import com.vmware.connection.ConnectedVimServiceBase;

import com.vmware.vim25.AboutInfo;
import com.vmware.vim25.ArrayOfManagedObjectReference;
import com.vmware.vim25.HostConfigInfo;
import com.vmware.vim25.HostConfigManager;
import com.vmware.vim25.HostNetworkInfo;
import com.vmware.vim25.HostProxySwitch;
import com.vmware.vim25.HostVirtualSwitch;
import com.vmware.vim25.InvalidPropertyFaultMsg;
import com.vmware.vim25.ManagedObjectReference;
import com.vmware.vim25.PhysicalNic;
import com.vmware.vim25.RetrieveOptions;
import com.vmware.vim25.RuntimeFaultFaultMsg;
import com.vmware.vim25.ServiceContent;
import com.vmware.vsan.connection.VimConnection;
import com.vmware.vsan.connection.VsanHealthConnection;
import com.vmware.vsan.sdk.ClusterComputeResourceDvsProfile;
import com.vmware.vsan.sdk.ClusterComputeResourceDvsProfileDVPortgroupSpecToServiceMapping;
import com.vmware.vsan.sdk.ClusterComputeResourceHCIConfigSpec;
import com.vmware.vsan.sdk.ClusterComputeResourceHostConfigurationInput;
import com.vmware.vsan.sdk.ClusterComputeResourceHostConfigurationProfile;
import com.vmware.vsan.sdk.ClusterComputeResourceVCProfile;
import com.vmware.vsan.sdk.ClusterConfigSpecEx;
import com.vmware.vsan.sdk.DVPortgroupConfigSpec;
import com.vmware.vsan.sdk.DuplicateNameFaultMsg;
import com.vmware.vsan.sdk.FolderNewHostSpec;
import com.vmware.vsan.sdk.HostConnectFaultFaultMsg;
import com.vmware.vsan.sdk.HostConnectSpec;
import com.vmware.vsan.sdk.HostLockdownMode;
import com.vmware.vsan.sdk.HostNtpConfig;
import com.vmware.vsan.sdk.InvalidLoginFaultMsg;
import com.vmware.vsan.sdk.InvalidNameFaultMsg;
import com.vmware.vsan.sdk.NotFoundFaultMsg;
import com.vmware.vsan.sdk.NotSupportedFaultMsg;
import com.vmware.vsan.sdk.SDDCBase;
import com.vmware.vsan.sdk.VimVsanReconfigSpec;
import com.vmware.vsan.sdk.VsanClusterConfigInfo;
import com.vmware.vsan.sdk.VsanClusterHealthSummary;
import com.vmware.vsan.sdk.VsanClusterHealthSystemStatusResult;
import com.vmware.vsan.sdk.VsanDataEfficiencyConfig;
import com.vmware.vsan.sdk.VsanDataEncryptionConfig;
import com.vmware.vsan.sdk.VsanFaultFaultMsg;
import com.vmware.vsan.sdk.VsanHostHealthSystemStatusResult;
import com.vmware.vsan.sdk.VsanhealthPortType;
import com.vmware.vsan.sdk.VsanhealthVimPortType;
import com.vmware.vsan.util.VsanUtil;

/**
 * <pre>
 * ConfigureHciSample
 *
 * This file includes sample codes for accessing both vSAN APIs and vSphere APIs
 * by using different connections to vSAN service and vSphere service.
 *
 * This example demonstrates how to call the vSphere HCI API via vSAN SDK.
 *
 * <b>Parameters:</b>
 * url          [required] : URL of the vCenter Web Service
 * username     [required] : Username for the vCenter authentication
 * password     [required] : Password for the vCenter authentication
 * hostIps      [required] : IPs of the hosts to be added to the cluster
 * hostUsername [required] : Username of the hosts
 * hostPassword [required] : Password of the hosts
 *
 * <b>Command Line:</b>
 * run.bat com.vmware.vsan.samples.ConfigureHciSample
 *    --url [webserviceurl]
 *    --username [username]
 *    --password [password]
 *    --hostIps [name]
 *    --hostUsername [hostUsername]
 *    --hostPassword [hostPassword]
 * </pre>
 */

@Sample(name = "ConfigureHciSample", description = "This sample code demonstrates how to call vSphere HCI API")
public class ConfigureHciSample extends ConnectedVimServiceBase {
   private final String datacenterName = "Datacenter";
   private final String clusterName = "HCI-Cluster";

   private VimConnection vimConnection;
   private String[] hostIps;
   private String hostUsername;
   private String hostPassword;

   @Option(name = "hostIps", required = true, description = "The IPs of the hosts, splitted by commar")
   public void sethostIps(String hostIps) {
      this.hostIps = hostIps.split(",");
   }

   @Option(name = "hostUsername", required = true, description = "The username for the hosts to be added")
   public void sethostUsername(String hostUsername) {
      this.hostUsername = hostUsername;
   }

   @Option(name = "hostPassword", required = true, description = "The password for the hosts to be added")
   public void sethostPassword(String hostPassword) {
      this.hostPassword = hostPassword;
   }

   /**
    * Get a connection to vSphere service which allows to call standard vSphere APIs.
    *
    * VsanHealthConnection should be used to call vSAN APIs and this connection is used for vSphere
    * APIs.
    *
    */
   public synchronized VimConnection getVimConnection() {
      if (vimConnection == null) {
         VsanHealthConnection connection = (VsanHealthConnection) super.connection;
         vimConnection = new VimConnection(connection);
      }
      // It will skip reconnection if it is already connected.
      vimConnection.connect();
      bumpVimVersionForConnection(vimConnection);
      return vimConnection;
   }

   private void bumpVimVersionForConnection(VimConnection vimConnection) {
      VsanhealthVimPortType vimPort = vimConnection.getVimPort();
      Map<String, Object> ctxt = ((BindingProvider) vimPort).getRequestContext();
      String version = VimConnection.getVimVmodlVersion(vimConnection.getURL().getHost());
      ctxt.put(BindingProvider.SOAPACTION_USE_PROPERTY, true);
      ctxt.put(BindingProvider.SOAPACTION_URI_PROPERTY, "urn:vim25/" + version);
   }

   private ManagedObjectReference getEntityFromName(ServiceContent serviceContent,
         String entityType, String entityName)
         throws InvalidPropertyFaultMsg, RuntimeFaultFaultMsg {
      Map<String, ManagedObjectReference> entities =
            getMOREFs.inContainerByType(serviceContent.getRootFolder(), entityType);
      for (String eName : entities.keySet()) {
         if (eName.equals(entityName)) {
            return entities.get(entityName);
         }
      }
      return null;
   }

   private void MonitorTask(com.vmware.vsan.sdk.ManagedObjectReference task, String taskname) {
      ManagedObjectReference vcTask = VsanUtil.ConvertToVcMoRef(task);

      // Here is an example of how to track a task returned by the vSphere API.
      Boolean status = VsanUtil.WaitForTasks(this.waitForValues, vcTask);
      if (status) {
         System.out.println(taskname + " completed successfully!");
      } else {
         System.out.println(taskname + " failed!");
      }
   }

   private String getSslThumbprint(String host) {
      String sslThumbPrint = "";
      try {
         X509TrustManager tm = new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] xcs, String string)
                  throws CertificateException {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] xcs, String string)
                  throws CertificateException {
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
               return null;
            }
         };
         SSLContext sc = SSLContext.getInstance("TLS");
         sc.init(null, new TrustManager[] { tm }, null);
         SSLSocketFactory factory = sc.getSocketFactory();
         SSLSocket socket = (SSLSocket) factory.createSocket(host, 443);
         SSLSession session = socket.getSession();
         java.security.cert.Certificate[] servercerts = session.getPeerCertificates();
         String digest = DatatypeConverter
               .printHexBinary(
                     MessageDigest.getInstance("SHA-1").digest(servercerts[0].getEncoded()))
               .toUpperCase();
         sslThumbPrint = colonify(digest.toCharArray());
      } catch (Exception e) {
         System.out.println("exception in get ssl " + e);
      }
      return sslThumbPrint;
   }

   private String colonify(char[] bytes) {
      Objects.requireNonNull(bytes, "byte array is null");
      if (bytes.length == 0) {
         return "";
      }

      final char[] chars = new char[bytes.length / 2 * 3 - 1];

      int charPos = 0;
      for (int pos = 0; pos < chars.length; pos++) {
         if (pos % 3 != 2) {
            chars[pos] = bytes[charPos];
            charPos++;
         } else {
            chars[pos] = ':';
         }
      }
      return String.valueOf(chars);
   }

   private ClusterComputeResourceHostConfigurationProfile CreateHostConfigProfile(String ntpServer,
         HostLockdownMode lockdownMode) {
      HostNtpConfig hostNtpConfig = new HostNtpConfig();
      hostNtpConfig.getServer().add(ntpServer);
      com.vmware.vsan.sdk.HostDateTimeConfig dateTimeConfig =
            new com.vmware.vsan.sdk.HostDateTimeConfig();
      dateTimeConfig.setNtpConfig(hostNtpConfig);

      ClusterComputeResourceHostConfigurationProfile hostConfigurationProfile =
            new ClusterComputeResourceHostConfigurationProfile();
      hostConfigurationProfile.setDateTimeConfig(dateTimeConfig);
      hostConfigurationProfile.setLockdownMode(lockdownMode);
      return hostConfigurationProfile;
   }

   private ClusterComputeResourceVCProfile GetVcProf() {
      com.vmware.vsan.sdk.ClusterDrsConfigInfo drsConfigInfo =
            new com.vmware.vsan.sdk.ClusterDrsConfigInfo();
      drsConfigInfo.setEnabled(true);
      drsConfigInfo.setVmotionRate(2);
      drsConfigInfo.setDefaultVmBehavior(com.vmware.vsan.sdk.DrsBehavior.FULLY_AUTOMATED);
      ClusterComputeResourceVCProfile vcProfile = new ClusterComputeResourceVCProfile();
      ClusterConfigSpecEx configSpecEx = new ClusterConfigSpecEx();

      configSpecEx.setDrsConfig(drsConfigInfo);
      vcProfile.setClusterSpec(configSpecEx);
      vcProfile.setEvcModeKey("intel-merom");
      return vcProfile;
   }

   private SDDCBase CreateDefaultVsanSpec() {
      VsanDataEfficiencyConfig dedupConfig = new VsanDataEfficiencyConfig();
      dedupConfig.setDedupEnabled(false);
      VsanDataEncryptionConfig encryptionConfig = new VsanDataEncryptionConfig();
      encryptionConfig.setEncryptionEnabled(false);

      VsanClusterConfigInfo vsanClusterConfigInfo = new VsanClusterConfigInfo();
      vsanClusterConfigInfo.setEnabled(true);
      com.vmware.vsan.sdk.VsanClusterConfigInfoHostDefaultInfo hostDefaultInfo =
            new com.vmware.vsan.sdk.VsanClusterConfigInfoHostDefaultInfo();

      VimVsanReconfigSpec vsanSpec = new VimVsanReconfigSpec();
      hostDefaultInfo.setAutoClaimStorage(false);
      vsanClusterConfigInfo.setDefaultConfig(hostDefaultInfo);
      vsanSpec.setDataEfficiencyConfig(dedupConfig);
      vsanSpec.setDataEncryptionConfig(encryptionConfig);
      vsanSpec.setAllowReducedRedundancy(false);
      vsanSpec.setModify(true);
      vsanSpec.setVsanClusterConfig(vsanClusterConfigInfo);

      return vsanSpec;
   }

   private ClusterComputeResourceDvsProfile getDvsProfile(ManagedObjectReference hostFolderMor, String hostname)
         throws RuntimeFaultFaultMsg, InvalidPropertyFaultMsg {
      String freePnic = GetFreePnicList(hostFolderMor, hostname);
      Map<String, String> dvpgNameAndService = new HashMap<String, String>();
      dvpgNameAndService.put("vmotion-dvpg", "vmotion");
      dvpgNameAndService.put("vsan-dvpg", "vsan");
      String dvsName = "hci-dvs-new";

      ClusterComputeResourceDvsProfile dvsProfile =
            createDvsProfile(dvpgNameAndService, freePnic, dvsName);
      return dvsProfile;
   }

   private String GetFreePnicList(ManagedObjectReference hostFolderMor, String hostname)
         throws RuntimeFaultFaultMsg, InvalidPropertyFaultMsg {
      ManagedObjectReference hostmor = null;
      if (hostFolderMor != null && hostname !=null) {
         Map<String, ManagedObjectReference> hostResults =
               getMOREFs.inFolderByType(hostFolderMor, "HostSystem", new RetrieveOptions());
         hostmor = hostResults.get(hostname);
      }

      if (hostmor != null) {
         HostConfigManager configMgr = (HostConfigManager) getMOREFs
               .entityProps(hostmor, new String[] { "configManager" }).get("configManager");
         ManagedObjectReference nwSystemMor = configMgr.getNetworkSystem();
         HostNetworkInfo netInfo = (HostNetworkInfo) getMOREFs
               .entityProps(nwSystemMor, new String[] { "networkInfo" }).get("networkInfo");
         List<String> allUpPnicsList = new ArrayList<String>();
         for (PhysicalNic pnic : netInfo.getPnic()) {
            if (pnic.getSpec().getLinkSpeed() != null) {
               allUpPnicsList.add(pnic.getDevice());
            }
         }
         List<String> usedNicsOnVss = new ArrayList<String>();
         for (HostVirtualSwitch vSwitch : netInfo.getVswitch()) {
            if (vSwitch.getSpec().getBridge() != null && vSwitch.getPnic().size() != 0) {
               usedNicsOnVss.addAll(vSwitch.getPnic());
            }
         }

         HostConfigInfo hostConfig = (HostConfigInfo) getMOREFs
               .entityProps(hostmor, new String[] { "config" }).get("config");

         String usedNicsOnProxy = null;
         List<HostProxySwitch> proxySwitches = hostConfig.getNetwork().getProxySwitch();
         for (HostProxySwitch proxySwitch : proxySwitches) {
            if (proxySwitch.getPnic() != null && proxySwitch.getPnic().size() > 0) {
               usedNicsOnProxy = proxySwitch.getPnic().get(0);
               break;
            }
         }

         List<String> usedVssPnics = new ArrayList<String>();
         if (usedNicsOnVss.size() >= 1) {
            //          In this case, usedVnicsOnVss returns an array of type:
            //          [(str) [ 'vmnic0' ], (str) [ 'vmnic5' ]]
            //          To obtain the entire list of vmnics, we need to read the first
            //          element saved in pyVmomi.VmomiSupport.str[].
            usedVssPnics.add(usedNicsOnVss.get(0));
         }
         if (usedNicsOnProxy != null) {
            String[] pnicsOnProxyList = usedNicsOnProxy.split("-", -1);
            String pnicsOnProxy = pnicsOnProxyList[pnicsOnProxyList.length - 1];
            usedVssPnics.add(pnicsOnProxy);
         }
         List<String> freePnics = allUpPnicsList.stream().filter(e -> !usedVssPnics.contains(e))
               .collect(Collectors.toList());


         String freePnic = "";
         if (freePnics != null && freePnics.size() > 0) {
            freePnic = freePnics.get(0);
         }
         return freePnic;
      } else {
         return null;
      }
   }

   private ClusterComputeResourceDvsProfile createDvsProfile(
         Map<String, String> dvpgNameAndServices, String freePnic, String dvsName) {
      ClusterComputeResourceDvsProfile dvsProfile = new ClusterComputeResourceDvsProfile();
      dvsProfile.setDvsName(dvsName);
      dvsProfile.setDvSwitch(null);
      dvsProfile.getPnicDevices().add(freePnic);

      List<ClusterComputeResourceDvsProfileDVPortgroupSpecToServiceMapping> dvpgToServiceMappings =
            new ArrayList<ClusterComputeResourceDvsProfileDVPortgroupSpecToServiceMapping>();

      for (Map.Entry<String, String> nameAndService : dvpgNameAndServices.entrySet()) {
         ClusterComputeResourceDvsProfileDVPortgroupSpecToServiceMapping dvpgToServiceMapping =
               new ClusterComputeResourceDvsProfileDVPortgroupSpecToServiceMapping();

         DVPortgroupConfigSpec dvpgConfigSpec = new DVPortgroupConfigSpec();
         dvpgConfigSpec.setNumPorts(128);
         dvpgConfigSpec.setName(nameAndService.getKey());
         dvpgConfigSpec.setType("earlyBinding");
         dvpgToServiceMapping.setDvPortgroupSpec(dvpgConfigSpec);
         dvpgToServiceMapping.setService(nameAndService.getValue());
         dvpgToServiceMappings.add(dvpgToServiceMapping);
      }
      dvsProfile.getDvPortgroupMapping().addAll(dvpgToServiceMappings);
      return dvsProfile;
   }

   /**
    * This method demonstrates how to get vSAN cluster health status by invoking the
    * QueryClusterHealthSummary API from vSAN health service against VC, then call a vSphere API to
    * configure HCI cluster from vSphere service.
    *
    * @throws RuntimeFaultFaultMsg
    * @throws NotFoundFaultMsg
    * @throws NotSupportedFaultMsg
    * @throws VsanFaultFaultMsg
    * @throws InvalidPropertyFaultMsg
    * @throws RuntimeFaultFaultMsg
    * @throws HostConnectFaultFaultMsg
    * @throws DuplicateNameFaultMsg
    * @throws InvalidLoginFaultMsg
    * @throws InvalidNameFaultMsg
    * @throws com.vmware.vsan.sdk.RuntimeFaultFaultMsg
    */
   @Action
   public void main()
         throws NotFoundFaultMsg, NotSupportedFaultMsg, VsanFaultFaultMsg, InvalidPropertyFaultMsg,
         RuntimeFaultFaultMsg, DuplicateNameFaultMsg, HostConnectFaultFaultMsg, InvalidNameFaultMsg,
         com.vmware.vsan.sdk.RuntimeFaultFaultMsg, InvalidLoginFaultMsg {
      AboutInfo aboutInfo = serviceContent.getAbout();

      if (!aboutInfo.getApiType().equals("VirtualCenter")) {
         System.out.println("Configure HCI API is only supported on vCenter");
         System.exit(1);
      }

      String majorApiVersion = aboutInfo.getApiVersion().split("\\.")[0];
      if (Integer.parseInt(majorApiVersion) < 6) {
         System.out.format(
               "The Virtual Center with version %s " + "(lower than 6.0) is not supported.",
               aboutInfo.getApiVersion());
         return;
      }

      VimConnection vimConnection = getVimConnection();
      VsanhealthVimPortType vimPort = vimConnection.getVimPort();

      vimPort.createDatacenter(VsanUtil.ConvertToHealthMoRef(serviceContent.getRootFolder()),
            this.datacenterName);

      ManagedObjectReference dcMor =
            getEntityFromName(serviceContent, "Datacenter", this.datacenterName);
      ManagedObjectReference hostFolderMor = (ManagedObjectReference) getMOREFs
            .entityProps(dcMor, new String[] { "hostFolder" }).get("hostFolder");
      ClusterConfigSpecEx clusterConfigSpec = new ClusterConfigSpecEx();
      clusterConfigSpec.setInHciWorkflow(true);
      VsanClusterConfigInfo vsanConfig = new VsanClusterConfigInfo();
      vsanConfig.setEnabled(true);
      clusterConfigSpec.setVsanConfig(vsanConfig);
      vimPort.createClusterEx(VsanUtil.ConvertToHealthMoRef(hostFolderMor), this.clusterName,
            clusterConfigSpec);

      ManagedObjectReference cluster =
            getEntityFromName(serviceContent, "ClusterComputeResource", this.clusterName);
      List<FolderNewHostSpec> hostSpecList = new ArrayList<FolderNewHostSpec>();
      for (String hostIp : hostIps) {
         FolderNewHostSpec folderHostSpec = new FolderNewHostSpec();
         HostConnectSpec hostSpec = new HostConnectSpec();
         hostSpec.setHostName(hostIp);
         hostSpec.setUserName(this.hostUsername);
         hostSpec.setPassword(this.hostPassword);
         hostSpec.setSslThumbprint(this.getSslThumbprint(hostIp));
         folderHostSpec.setHostCnxSpec(hostSpec);
         hostSpecList.add(folderHostSpec);

         com.vmware.vsan.sdk.ManagedObjectReference task = vimPort.batchAddHostsToClusterTask(
               VsanUtil.ConvertToHealthMoRef(hostFolderMor), VsanUtil.ConvertToHealthMoRef(cluster),
               hostSpecList, null, null, "maintenance");
         String taskName = "Add host " + hostIp;
         MonitorTask(task, taskName);
      }

      // Configure HCI Spec
      ClusterComputeResourceHCIConfigSpec hciSpec = new ClusterComputeResourceHCIConfigSpec();

      String NTP_SERVER = "time-c-b.nist.gov";
      ClusterComputeResourceHostConfigurationProfile hostConfigProfile =
            CreateHostConfigProfile(NTP_SERVER, HostLockdownMode.LOCKDOWN_DISABLED);
      hciSpec.setHostConfigProfile(hostConfigProfile);

      ClusterComputeResourceVCProfile vcProfile = GetVcProf();
      hciSpec.setVcProf(vcProfile);

      VsanClusterConfigInfo vSanConfigInfo = new VsanClusterConfigInfo();
      SDDCBase vsanConfigSpecBase = CreateDefaultVsanSpec();
      hciSpec.setVSanConfigSpec(vsanConfigSpecBase);

      ArrayOfManagedObjectReference hostMors = (ArrayOfManagedObjectReference) getMOREFs
            .entityProps(cluster, new String[] { "host" }).get("host");
      ClusterComputeResourceDvsProfile dvsProfile = getDvsProfile(hostFolderMor, hostIps[0]);
      hciSpec.getDvsProf().add(dvsProfile);

      // Do the same configuration on all the hosts inside this cluster.
      List<ClusterComputeResourceHostConfigurationInput> hostCfgs =
            new ArrayList<ClusterComputeResourceHostConfigurationInput>();
      for (ManagedObjectReference host : hostMors.getManagedObjectReference()) {
         ClusterComputeResourceHostConfigurationInput hostCfg =
               new ClusterComputeResourceHostConfigurationInput();
         hostCfg.setHost(VsanUtil.ConvertToHealthMoRef(host));
         hostCfgs.add(hostCfg);
      }

      com.vmware.vsan.sdk.ManagedObjectReference hciTask =
            vimPort.configureHCITask(VsanUtil.ConvertToHealthMoRef(cluster), hciSpec, hostCfgs);

      // Need covert to vcTask to bind the managed object with VC session.
      MonitorTask(hciTask, "Configure HCI");
      System.out.println("start health check... ");
      VsanHealthConnection connection = (VsanHealthConnection) super.connection;
      VsanhealthPortType healthPort = connection.getVsanHealthPort();
      Boolean fetchFromCache = false;
      Boolean includeObjUuid = true;
      VsanClusterHealthSummary healthSummary = healthPort.vsanQueryVcClusterHealthSummary(
            connection.getVsanVcHealthServiceInstanceReference(),
            VsanUtil.ConvertToHealthMoRef(cluster), null, null, includeObjUuid, null,
            fetchFromCache, null, null, null);
      VsanClusterHealthSystemStatusResult clusterStatus = healthSummary.getClusterStatus();
      System.out.println("Cluster " + clusterName + " status: " + clusterStatus.getStatus());
      for (VsanHostHealthSystemStatusResult hostStatus : clusterStatus.getTrackedHostsStatus()) {
         System.out
               .println("Host " + hostStatus.getHostname() + " status: " + hostStatus.getStatus());
      }
   }
}
