using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Net;
using System.Net.Security;
using System.Reflection;
using System.Web.Services.Protocols;
using System.Threading;
using System.Text.RegularExpressions;
using ConverterApi;

namespace ConverterSamples
{
   class ConverterConnection
   {
      private ConverterService _converterService = null;
      private ConverterServerContent _converterServerContent = null;
      private ConverterUserSession _userSession = null;
      private int _waitSeconds = 180;
      private int _totalAttempts = 5;



      /// <summary>
      /// Connect to a converter server
      /// </summary>
      /// <param name="serverUrl">Server URL</param>
      /// <param name="username">User name to log in</param>
      /// <param name="password">Passsword to log in</param>
      public void
      Connect(string serverUrl, string username, string password)
      {
         ManagedObjectReference morServiceInstance = new ManagedObjectReference();
         morServiceInstance.type = "ConverterServiceInstance";
         morServiceInstance.Value = "ServiceInstance";

         _converterService = new ConverterService();
         _converterService.Url = serverUrl;
         _converterService.CookieContainer = new System.Net.CookieContainer();
         ServicePointManager.ServerCertificateValidationCallback =
            new RemoteCertificateValidationCallback(RemoteCertificateValidation);

         _converterServerContent = (ConverterServerContent)
            _converterService.ConverterRetrieveServiceContent(morServiceInstance);

         if (_converterServerContent.sessionManager == null) {
            throw new Exception("Session manager is null");
         }

         _userSession = _converterService.ConverterLogin(_converterServerContent.sessionManager,
                                                         username, password, null);

         if (_userSession == null) {
            throw new Exception("User session is null");
         }
      }

      /// <summary>
      /// Query the hardware and OS info of a source machine.
      /// Can prompt the user to confirm the machine's SSL thumbprint.
      /// Side effect: will cache the thumbprint in computerSpec.
      /// </summary>
      /// <param name="computerSpec">The machine to query.
      /// <returns>The source info</returns>
      public ConverterComputerInfo
      QuerySource(ConverterComputerSpec computerSpec)
      {
         ConverterComputerInfo result = null;
         bool retry;

         do {
            retry = false;

            try {
               result = _converterService.ConverterQuery(_converterServerContent.queryManager,
                                                         computerSpec);
            } catch (SoapException e) {
               String remoteThumbprint =
                  ExtractCertificateThumbprintFromFaultMessage(e.Detail.InnerText);
               if (remoteThumbprint != null) {
                  retry = CheckCertificate(computerSpec.location, remoteThumbprint);
               }

               if (!retry) {
                  throw;
               }
            }
         } while (retry);

         return result;
      }

      /// <summary>
      /// Validate a source machine against the destination host and
      /// get default conversion parameters.
      /// Will not return defaults if validation fails.
      /// Can prompt the user to confirm the machine's SSL thumbprint.
      /// Side effect: will cache the thumbprint in computerSpec.
      /// </summary>
      /// <param name="source">The source machine.
      /// <param name="targetVmSpec">The destination host.
      /// <returns>The default parameters</returns>
      public ConverterDefaultConversionParamsResult
      ValidateTargetAndGetDefaults(ConverterComputerSpec source,
                                   ConverterTargetVmSpec targetVmSpec)
      {
         ConverterDefaultConversionParamsResult result = null;
         bool retry;

         do {
            retry = false;

            try {
               result = _converterService.ConverterValidateTargetAndGetDefaults(
                  _converterServerContent.queryManager, source, targetVmSpec);
            } catch (SoapException e) {
               String remoteThumbprint =
                  ExtractCertificateThumbprintFromFaultMessage(e.Detail.InnerText);

               if (remoteThumbprint != null) {
                  // Check source certificate
                  retry = CheckCertificate(source.location, remoteThumbprint);

                  if (!retry) {
                     // Check target certificate
                     retry = CheckCertificate(targetVmSpec.location, remoteThumbprint);
                  }
               }

               if (!retry) {
                  throw;
               }
            }
         }
         while (retry);

         return result;
      }

      /// <summary>
      /// Install converter agent on live Windows source machine.
      /// Can prompt the user to confirm the machine's SSL thumbprint.
      /// Side effect: will cache the thumbprint in computerSpec.
      /// </summary>
      /// <param name="liveComputerLocation">The source machine.
      /// <param name="sourceAgentPort">The port the agent is supposed to listen to</param>
      /// <param name="sourcePostponeReboot">In some cases the machine might need
      /// to be rebooted after installation. Whether to reboot immediately or not</param>
      /// <returns></returns>
      public void
      InstallAgent(ConverterComputerSpecLiveComputerLocation liveComputerLocation,
                   int sourceAgentPort, bool sourcePostponeReboot)
      {
         // Check for Agent existence.
         ConverterComputerSpec computerSpec = new ConverterComputerSpec();
         computerSpec.location = liveComputerLocation;

         bool retry;
         do {
            retry = false;
            try {
               _converterService.ConverterValidateAgentAvailability(
                  _converterServerContent.queryManager, computerSpec);
               return;
            } catch (SoapException e) {
               String remoteThumbprint = ExtractCertificateThumbprintFromFaultMessage(e.Message);
               if (remoteThumbprint != null) {
                  retry = CheckCertificate(computerSpec.location, remoteThumbprint);

                  if (!retry) {
                     throw;
                  }
               } else {
                  Console.WriteLine("Agent isn't installed on source machine {0} '" +
                                    e.Message + "'. Trying to install agent...",
                                    liveComputerLocation.hostname);
               }
            } catch (Exception e) {
               Console.WriteLine("Agent isn't installed on source machine {0} '" +
                                 e.Message + "'. Trying to install agent...",
                                 liveComputerLocation.hostname);
            }
         }
         while (retry);

         ConverterAgentManagerAgentDeploymentResult result =
            _converterService.ConverterInstallAgent(_converterServerContent.agentManager,
                                                    liveComputerLocation.hostname,
                                                    sourceAgentPort, true,
                                                    liveComputerLocation.username,
                                                    liveComputerLocation.password,
                                                    sourcePostponeReboot, true);

         // Update source computer spec thumbprint with the one returned after agent
         // deployment, if any
         liveComputerLocation.sslThumbprint = result.sslThumbprint;

         switch (result.status) {
            case ConverterAgentManagerDeploymentStatus.completed:
               return;
            case ConverterAgentManagerDeploymentStatus.rebootRequired:
               if (sourcePostponeReboot) {
                  throw new Exception("A reboot of the physical source " +
                                      liveComputerLocation.hostname +
                                      "is required for the agent installation to succeed. " +
                                      "Please try the P2V after rebooting the source");
               } else {
                  _converterService.ConverterRebootMachine(_converterServerContent.agentManager,
                                                           liveComputerLocation.hostname,
                                                           liveComputerLocation.username,
                                                           liveComputerLocation.password);
                  Console.WriteLine("A reboot of the physical source {0} has been initiated.",
                                    liveComputerLocation.hostname);
                  // Go to waiting
                  break;
               }

            case ConverterAgentManagerDeploymentStatus.rebootInitiated:
               // Go to waiting
               break;

            default:
               Debug.Fail("Unexpected deployment status");
               break;
         }

         // If a reboot of the physical source has been initiated.
         for (int attempt = 0; attempt < _totalAttempts; attempt++) {
            try {
               _converterService.ConverterValidateAgentAvailability(
                  _converterServerContent.queryManager, computerSpec);
               return;
            } catch (Exception e) {
               Console.WriteLine("Caught Exception : " + e.Message + " Trying again...");
               System.Threading.Thread.Sleep(_waitSeconds * 1000);
            }
         }

         throw new Exception(
            String.Format("Unable to validate agent availability after {0} attempts",
                          _totalAttempts));
      }

      /// <summary>
      /// Submits a conversion or reconfiguration job.
      /// </summary>
      /// <param name="jobSpec">The job spec object</param>
      /// <returns>Info about the submitted job</returns>
      public ConverterServerConversionConversionJobInfo
      SubmitJob(ConverterServerConversionConversionJobSpec jobSpec)
      {
         ConverterServerConversionConversionJobInfo result = null;
         bool retry;

         do {
            retry = false;

            try {
               result = _converterService.ConverterServerConversionManagerCreateJob(
                                                   _converterServerContent.conversionManager,
                                                   jobSpec,
                                                   null);
            } catch (SoapException e) {
               String remoteThumbprint =
                  ExtractCertificateThumbprintFromFaultMessage(e.Detail.InnerText);

               if (remoteThumbprint != null) {
                  // Check source certificate
                  retry = CheckCertificate(jobSpec.source.location, remoteThumbprint);

                  if (!retry) {
                     // Check target certificate
                     retry = CheckCertificate(jobSpec.conversionParams.cloningParams.target.location,
                                              remoteThumbprint);
                  }
               }

               if (!retry) {
                  throw;
               }
            }
         }
         while (retry);

         return result;
      }

      /// <summary>
      /// Updates a synchronizable job. This may start an incremental update now
      /// or schedule it for the future.
      /// </summary>
      /// <param name="jobId">The job id (as seen in converter client) to update </param>
      /// <param name="updateSpec">The spec for update</param>
      /// <returns>The old conversion parameters before the update</returns>
      public ConverterConversionParams
      UpdateJob(int jobId, ConverterServerConversionConversionParamsUpdateSpec updateSpec)
      {
         ManagedObjectReference job = new ManagedObjectReference();
         job.type = "ConverterServerConversionConversionJob";
         job.Value = "job-" + jobId.ToString();

         ConverterConversionParams result =
            _converterService.ConverterServerConversionJobUpdateConversionParams(
               job, updateSpec);

         return result;
      }

      /// <summary>
      /// Gets info for the last converter tasks
      /// </summary>
      /// <param name="tasks">Number of tasks to get</param>
      /// <returns>Task info array</returns>
      public ConverterTaskInfo[]
      GetTaskHistory(int tasks)
      {
         ConverterTaskFilterSpec filter = new ConverterTaskFilterSpec();
         // Customize filter if needed
         ManagedObjectReference taskCollector =
            _converterService.ConverterCreateCollectorForTasks(_converterServerContent.taskManager,
                                                               filter);

         return _converterService.ConverterReadNextTasks(taskCollector, tasks);
      }

      /// <summary>
      /// Gets info for the last converter jobs
      /// </summary>
      /// <param name="jobs">Number of jobs to get</param>
      /// <returns>Jobs info array</returns>
      public ConverterServerConversionConversionJobInfo[]
      GetJobHistory(int jobs)
      {
         ConverterServerJobJobFilterSpec filter = new ConverterServerJobJobFilterSpec();
         // Customize filter if needed
         ManagedObjectReference jobCollector = _converterService.ConverterCreateCollectorForJobs(
            _converterServerContent.conversionManager, filter);

         return _converterService.ConverterReadNextConversionJobs(jobCollector, jobs);
      }

      /// <summary>
      /// Prompts the user to accept the SSL thumbprint if not already set
      /// Utility function to call when a call to converter server returns an SSL fault.
      /// </summary>
      /// <param name="location">Machine to authenticate to</param>
      /// <param name="remoteThumbprint">Thumbprint to accept</param>
      /// <returns>Whether to retry the call (after accepting the thumbprint)</returns>
      private bool
      CheckCertificate(Object location, String remoteThumbprint)
      {
         String thumbprint = Common.GetThumbprint(location);
         String hostname = null;
         String label = null;

         if (location is ConverterComputerSpecLiveComputerLocation) {
            // Physical source
            ConverterComputerSpecLiveComputerLocation loc =
               (ConverterComputerSpecLiveComputerLocation)location;
            hostname = loc.hostname;
            label = "Live source machine";
         } else if (location is ConverterComputerSpecManagedVmLocation) {
            // Managed source
            ConverterComputerSpecManagedVmLocation loc =
               (ConverterComputerSpecManagedVmLocation)location;
            hostname = loc.vimConnect.hostname;
            label = "Source vCenter or ESX server";
         } else if (location is ConverterComputerSpecHyperVComputerLocation) {
            // Hyper-V source
            ConverterComputerSpecHyperVComputerLocation loc =
               (ConverterComputerSpecHyperVComputerLocation)location;
            hostname = loc.hostname;
            label = "Source Hyper-V server";
         } else if (location is ConverterTargetVmSpecManagedVmLocation) {
            // Managed target
            ConverterTargetVmSpecManagedVmLocation loc =
               (ConverterTargetVmSpecManagedVmLocation)location;
            hostname = loc.vimConnect.hostname;
            label = "Destination vCenter or ESX server";
         }

         bool result = false;

         // Location that has SSL thumbprint but is not set
         if (!String.IsNullOrEmpty(hostname) && thumbprint != remoteThumbprint) {
            String message =
                  String.Format("\n{0} certificate validation failed." +
                                "\n{0}: {1}" +
                                "\nCertificate thumbprint: {2}" +
                                "\nProceed with connecting to the destination server?",
                           label, hostname, remoteThumbprint);
            result = ReadYesNo(message, false);

            if (result) {
               if (location is ConverterComputerSpecLiveComputerLocation) {
                  ConverterComputerSpecLiveComputerLocation loc =
                     (ConverterComputerSpecLiveComputerLocation)location;
                  loc.sslThumbprint = remoteThumbprint;
               } else if (location is ConverterComputerSpecManagedVmLocation) {
                  ConverterComputerSpecManagedVmLocation loc =
                     (ConverterComputerSpecManagedVmLocation)location;
                  loc.vimConnect.sslThumbprint = remoteThumbprint;
               } else if (location is ConverterComputerSpecHyperVComputerLocation) {
                  ConverterComputerSpecHyperVComputerLocation loc =
                     (ConverterComputerSpecHyperVComputerLocation)location;
                  loc.sslThumbprint = remoteThumbprint;
               } else if (location is ConverterTargetVmSpecManagedVmLocation) {
                  ConverterTargetVmSpecManagedVmLocation loc =
                     (ConverterTargetVmSpecManagedVmLocation)location;
                  loc.vimConnect.sslThumbprint = remoteThumbprint;
               }
            }
         }

         return result;
      }

      /// <summary>
      /// Extract the thumbprint from the SSL fault
      /// </summary>
      /// <param name="message">Error message</param>
      /// <returns>SSL thumbprint</returns>
      private String
      ExtractCertificateThumbprintFromFaultMessage(String message)
      {
         // Thumbprints look like CC:49:57:9A:82:30:8F:3C:AA:3D:01:4C:FA:F6:50:FF:16:1F:C9:65
         //                    or d5:3b:ff:63:ef:d1:7d:d4:38:f1:11:1b:e6:7a:a7:8a
         const String X509_THUMBPRINT_RE = @"(([0-9a-fA-F]{2}:){19}[0-9a-fA-F]{2})";
         const String SSH_THUMBPRINT_RE = @"(([0-9a-fA-F]{2}:){15}[0-9a-fA-F]{2})";

         Match match = Regex.Match(message, X509_THUMBPRINT_RE, RegexOptions.None);
         if (match.Success) {
            return match.Value.Trim();
         } else {
            match = Regex.Match(message, SSH_THUMBPRINT_RE, RegexOptions.None);
            if (match.Success) {
               return match.Value.Trim();
            } else {
               return null;
            }
         }
      }

      /// <summary>
      /// Prompt the user to accept thumbprint
      /// </summary>
      /// <param name="prompt">Text for the user</param>
      /// <param name="defaultValue">Suggested answer</param>
      /// <returns>User's answer</returns>
      private bool
      ReadYesNo(String prompt, bool defaultValue)
      {
         Console.WriteLine(prompt);
         bool result = defaultValue;
         bool ready = false;

         do {
            Console.Write("(Enter [y]es, [n]o or [c]ancel): > ");

            String line;
            try {
               line = System.Console.ReadLine();
            } catch (IOException) {
               // Something went wrong
               break;
            }

            line = line.Trim().ToLower();
            if (line.CompareTo("y") == 0 ||
                  line.CompareTo("yes") == 0) {
               result = true;
               ready = true;
            } else if (line.CompareTo("n") == 0 ||
                       line.CompareTo("no") == 0) {
               result = false;
               ready = true;
            } else if (line.CompareTo("c") == 0 ||
                          line.CompareTo("cancel") == 0) {
               // Use default answer
               ready = true;
            }
         }
         while (!ready);

         return result;
      }

      /// <summary>
      /// Set certificate policy to allow all certificates
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="certificate"></param>
      /// <param name="chain"></param>
      /// <param name="sslPolicyErrors"></param>
      /// <returns></returns>
      private static bool
      RemoteCertificateValidation(
         Object sender,
         System.Security.Cryptography.X509Certificates.X509Certificate certificate,
         System.Security.Cryptography.X509Certificates.X509Chain chain,
         System.Net.Security.SslPolicyErrors sslPolicyErrors)
      {
         //Handle bad certificates here
         return true;
      }
   }
}
