require 'rbvmomi'

module ChefProvisioningVsphere
  class VsphereHelper

    if !$guest_op_managers
      $guest_op_managers = {}
    end

    def initialize(connect_options, datacenter_name)
      @connect_options = connect_options
      @datacenter_name = datacenter_name
    end

    attr_reader :connect_options
    attr_reader :datacenter_name

    def vim
      if @current_connection.nil? or @current_connection.serviceContent.sessionManager.currentSession.nil?
        @datacenter = nil
        puts "establishing connection to #{connect_options[:host]}"
        @current_connection = RbVmomi::VIM.connect connect_options
        str_conn = @current_connection.pretty_inspect # a string in the format of VIM(host ip)
        
        # we are caching guest operation managers in a global variable...terrible i know
        # this object is available from the serviceContent object on API version 5 forward
        # Its a singleton and if another connection is made for the same host and user
        # that object is not available on any subsequent connection
        # I could find no documentation that discusses this
        if !$guest_op_managers.has_key?(str_conn)
          $guest_op_managers[str_conn] = @current_connection.serviceContent.guestOperationsManager
        end
      end

      @current_connection
    end

    def find_vm(folder, vm_name)
      folder = find_folder(folder) unless folder.is_a? RbVmomi::VIM::Folder
      folder.find(vm_name, RbVmomi::VIM::VirtualMachine)
    end

    def find_vm_by_id(uuid)
      vm = vim.searchIndex.FindByUuid(
        uuid: uuid,
        vmSearch: true,
        instanceUuid: true
      )
    end

    def start_vm(vm, wait_on_port = 22)
      state = vm.runtime.powerState
      unless state == 'poweredOn'
        vm.PowerOnVM_Task.wait_for_completion
      end
    end

    def stop_vm(vm, timeout = 600)
        start = Time.now.utc
      begin
        vm.ShutdownGuest
        until (Time.now.utc - start) > timeout ||
          vm.runtime.powerState == 'poweredOff' do
            print '.'
            sleep 2
        end
      rescue
        vm.PowerOffVM_Task.wait_for_completion
      end
    end

    #folder could be like:  /Level1/Level2/folder_name
    def find_folder(folder_name)
      base = datacenter.vmFolder
      unless folder_name.nil?
        folder_name.split('/').reject(&:empty?).each do |item|
          base = base.find(item, RbVmomi::VIM::Folder) ||
            raise("vSphere Folder not found [#{folder_name}]")
        end
      end
      base
    end

    def datacenter
      vim # ensure connection is valid
      @datacenter ||= begin
        rootFolder = vim.serviceInstance.content.rootFolder
        dc = rootFolder.childEntity.grep(RbVmomi::VIM::Datacenter).find do |x|
          x.name == datacenter_name
        end
        raise("vSphere Datacenter not found [#{datacenter_name}]") if dc.nil?
        dc
      end
    end

    def network_adapter_for(operation, network_name, network_label, device_key, backing_info)
      connectable = RbVmomi::VIM::VirtualDeviceConnectInfo(
        :allowGuestControl => true,
        :connected => true,
        :startConnected => true)
      device = RbVmomi::VIM::VirtualVmxnet3(
        :backing => backing_info,
        :deviceInfo => RbVmomi::VIM::Description(:label => network_label, :summary => network_name.split('/').last),
        :key => device_key,
        :connectable => connectable)
      RbVmomi::VIM::VirtualDeviceConfigSpec(
        :operation => operation,
        :device => device)
    end

    def find_ethernet_cards_for(vm)
      vm.config.hardware.device.select {|d| d.is_a?(RbVmomi::VIM::VirtualEthernetCard)}
    end

    def add_extra_nic(action_handler, vm_template, options, vm)
      deviceAdditions, changes = network_device_changes(action_handler, vm_template, options)

      if deviceAdditions.count > 0
        current_networks = find_ethernet_cards_for(vm).map{|card| network_id_for(card.backing)}
        new_devices = deviceAdditions.select { |device| !current_networks.include?(network_id_for(device.device.backing))}
        
        if new_devices.count > 0
          action_handler.report_progress "Adding extra NICs"
          task = vm.ReconfigVM_Task(:spec => RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => new_devices))
          task.wait_for_completion
          new_devices
        end
      end
    end

    def network_id_for(backing_info)
      if backing_info.is_a?(RbVmomi::VIM::VirtualEthernetCardDistributedVirtualPortBackingInfo)
        backing_info.port.portgroupKey
      else
        backing_info.deviceName
      end
    end

    def create_delta_disk(vm_template)
      disks = vm_template.config.hardware.device.grep(RbVmomi::VIM::VirtualDisk)
      disks.select { |disk| disk.backing.parent == nil }.each do |disk|
        spec = {
          :deviceChange => [
            {
              :operation => :remove,
              :device => disk
            },
            {
              :operation => :add,
              :fileOperation => :create,
              :device => disk.dup.tap { |new_disk|
                new_disk.backing = new_disk.backing.dup
                new_disk.backing.fileName = "[#{disk.backing.datastore.name}]"
                new_disk.backing.parent = disk.backing
              },
            }
          ]
        }
        vm_template.ReconfigVM_Task(:spec => spec).wait_for_completion
        end
      end

    def virtual_disk_for(vm, datastore, size_gb)
      idx = vm.disks.count
      RbVmomi::VIM::VirtualDeviceConfigSpec(
        :operation     => :add,
        :fileOperation => :create,
        :device        => RbVmomi::VIM.VirtualDisk(
          :key           => idx,
          :backing       => RbVmomi::VIM.VirtualDiskFlatVer2BackingInfo(
            :fileName        => "[#{datastore}]",
            :diskMode        => 'persistent',
            :thinProvisioned => true
          ),
          :capacityInKB  => size_gb * 1024 * 1024,
          :controllerKey => 1000,
          :unitNumber    => idx
        )
      )
    end

    def network_device_changes(action_handler, vm_template, options)
      additions = []
      changes = []
      networks=options[:network_name]
      if networks.kind_of?(String)
        networks=[networks]
      end

      cards = find_ethernet_cards_for(vm_template)

      key = 4000
      networks.each_index do | i |
        label = "Ethernet #{i+1}"
        backing_info = backing_info_for(action_handler, networks[i])
        if card = cards.shift
          key = card.key
          operation = RbVmomi::VIM::VirtualDeviceConfigSpecOperation('edit')
          action_handler.report_progress "changing template nic for #{networks[i]}"
          changes.push(
            network_adapter_for(operation, networks[i], label, key, backing_info))
        else
          key = key + 1
          operation = RbVmomi::VIM::VirtualDeviceConfigSpecOperation('add')
          action_handler.report_progress "will be adding nic for #{networks[i]}"
          additions.push(
            network_adapter_for(operation, networks[i], label, key, backing_info))
        end
      end
      [additions, changes]
    end

    def backing_info_for(action_handler, network_name)
      action_handler.report_progress('finding networks...')
      network = find_network(network_name)
      action_handler.report_progress(
        "network: #{network_name} is a #{network.class}")
      if network.is_a?(RbVmomi::VIM::DistributedVirtualPortgroup)
        port = RbVmomi::VIM::DistributedVirtualSwitchPortConnection(
          :switchUuid => network.config.distributedVirtualSwitch.uuid,
          :portgroupKey => network.key
        )
        RbVmomi::VIM::VirtualEthernetCardDistributedVirtualPortBackingInfo(
          :port => port)
      else
        RbVmomi::VIM::VirtualEthernetCardNetworkBackingInfo(
          deviceName: network_name.split('/').last)
      end
    end

    def find_datastore(datastore_name)
      datacenter.datastore.find { |f| f.info.name == datastore_name } or raise "no such datastore #{datastore_name}"
    end

    def find_entity(name, parent_folder, &block)
      parts = name.split('/').reject(&:empty?)
      parts.each do |item|
        Chef::Log.debug("Identifying entity part: #{item} in folder type: #{parent_folder.class}")
        if parent_folder.is_a? RbVmomi::VIM::Folder
          Chef::Log.debug('Parent folder is a folder')
          parent_folder = parent_folder.childEntity.find { |f| f.name == item }
        else
          parent_folder = block.call(parent_folder, item)
        end
      end
      parent_folder
    end

    def find_host(host_name)
      host = find_entity(host_name, datacenter.hostFolder) do |parent, part|
        case parent
        when RbVmomi::VIM::ClusterComputeResource || RbVmomi::VIM::ComputeResource
          parent.host.find { |f| f.name == part }
        when RbVmomi::VIM::HostSystem
          parent.host.find { |f| f.name == part }
        else
          nil
        end
      end

      raise "vSphere Host not found [#{host_name}]" if host.nil?

      if host.is_a?(RbVmomi::VIM::ComputeResource)
        host = host.host.first
      end
      host
    end

    def find_pool(pool_name)
      Chef::Log.debug("Finding pool: #{pool_name}")
      pool = find_entity(pool_name, datacenter.hostFolder) do |parent, part|
        case parent
        when RbVmomi::VIM::ClusterComputeResource, RbVmomi::VIM::ComputeResource
          Chef::Log.debug("finding #{part} in a #{parent.class}: #{parent.name}")
          Chef::Log.debug("Parent root pool has #{parent.resourcePool.resourcePool.count} pools")
          parent.resourcePool.resourcePool.each { |p| Chef::Log.debug(p.name ) }
          parent.resourcePool.resourcePool.find { |f| f.name == part }
        when RbVmomi::VIM::ResourcePool
          Chef::Log.debug("finding #{part} in a Resource Pool: #{parent.name}")
          Chef::Log.debug("Pool has #{parent.resourcePool.count} pools")
          parent.resourcePool.each { |p| Chef::Log.debug(p.name ) }
          parent.resourcePool.find { |f| f.name == part }
        else
          Chef::Log.debug("parent of #{part} is unexpected type: #{parent.class}")
          nil
        end
      end

      raise "vSphere ResourcePool not found [#{pool_name}]" if pool.nil?

      if !pool.is_a?(RbVmomi::VIM::ResourcePool) && pool.respond_to?(:resourcePool)
        pool = pool.resourcePool
      end
      pool
    end

    def find_network(name)
      base = datacenter.networkFolder
      entity_array = name.split('/').reject(&:empty?)
      entity_array.each do |item|
        case base
        when RbVmomi::VIM::Folder
          base = base.find(item)
        when RbVmomi::VIM::VmwareDistributedVirtualSwitch
          idx = base.summary.portgroupName.find_index(item)
          base = idx.nil? ? nil : base.portgroup[idx]
        end
      end

      raise "vSphere Network not found [#{name}]" if base.nil?

      base
    end

    def find_customization_spec(customization_spec)
      csm = vim.serviceContent.customizationSpecManager
      csi = csm.GetCustomizationSpec(:name => customization_spec)
      spec = csi.spec
      raise "Customization Spec not found [#{customization_spec}]" if spec.nil?
      spec
    end

    def upload_file_to_vm(vm, username, password, local, remote)
      auth = RbVmomi::VIM::NamePasswordAuthentication({:username => username, :password => password, :interactiveSession => false})
      size = File.size(local)
      endpoint = $guest_op_managers[vim.pretty_inspect].fileManager.InitiateFileTransferToGuest(
        :vm => vm, 
        :auth => auth, 
        :guestFilePath => remote,
        :overwrite => true,
        :fileAttributes => RbVmomi::VIM::GuestWindowsFileAttributes.new,
        :fileSize => size)

        uri = URI.parse(endpoint)
        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
        
        req = Net::HTTP::Put.new("#{uri.path}?#{uri.query}")
        req.body_stream = File.open(local)
        req["Content-Type"] = "application/octet-stream"
        req["Content-Length"] = size
        res = http.request(req) 
        unless res.kind_of?(Net::HTTPSuccess)
          raise "Error: #{res.inspect} :: #{res.body} :: sending #{local} to #{remote} at #{vm.name} via #{endpoint} with a size of #{size}"
        end
    end
  end
end
