# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1

require "log4r"
require "fileutils"
require "vagrant/util/numeric"
require "vagrant/util/experimental"

module VagrantPlugins
  module HyperV
    module Cap
      module ConfigureDisks
        LOGGER = Log4r::Logger.new("vagrant::plugins::hyperv::configure_disks")

        # @param [Vagrant::Machine] machine
        # @param [VagrantPlugins::Kernel_V2::VagrantConfigDisk] defined_disks
        # @return [Hash] configured_disks - A hash of all the current configured disks
        def self.configure_disks(machine, defined_disks)
          return {} if defined_disks.empty?

          machine.ui.info(I18n.t("vagrant.cap.configure_disks.start"))

          current_disks = machine.provider.driver.list_hdds

          configured_disks = {disk: [], floppy: [], dvd: []}

          defined_disks.each do |disk|
            if disk.type == :disk
              disk_data = handle_configure_disk(machine, disk, current_disks)
              configured_disks[:disk] << disk_data if !disk_data.empty?
            elsif disk.type == :floppy
              # TODO: Write me
              machine.ui.info(I18n.t("vagrant.cap.configure_disks.floppy_not_supported", name: disk.name))
            elsif disk.type == :dvd
              disk_data = handle_configure_dvd(machine, disk)
              configured_disks[:dvd] << disk_data if !disk_data.empty?
            end
          end

          configured_disks
        end

        protected

        # @param [Vagrant::Machine] machine - the current machine
        # @param [Config::Disk] disk - the current disk to configure
        # @param [Array] all_disks - A list of all currently defined disks in VirtualBox
        # @return [Hash] current_disk - Returns the current disk. Returns nil if it doesn't exist
        def self.get_current_disk(machine, disk, all_disks)
          current_disk = nil
          if disk.primary
            # Ensure we grab the proper primary disk
            # We can't rely on the order of `all_disks`, as they will not
            # always come in port order, but primary should always be the
            # first disk.

            sorted_disks = all_disks.sort_by { |d|
              [d["ControllerNumber"].to_i, d["ControllerLocation"].to_i]
            }

            LOGGER.debug("sorted disks for primary detection: #{sorted_disks}")

            current_disk = sorted_disks.first

            # Need to get actual disk info to obtain UUID instead of what's returned
            #
            # This is not required for newly created disks, as its metadata is
            # set when creating and attaching the disk. This is only for the primary
            # disk, since it already exists.
            current_disk = machine.provider.driver.get_disk(current_disk["Path"])
          else
            # Hyper-V disk names aren't the actual names of the disk, so we have
            # to grab the name from the file path instead
            current_disk = all_disks.detect { |d| File.basename(d["Path"], '.*') == disk.name}
          end

          current_disk
        end

        # Handles all disk configs of type `:disk`
        #
        # @param [Vagrant::Machine] machine - the current machine
        # @param [Config::Disk] disk - the current disk to configure
        # @param [Array] all_disks - A list of all currently defined disks in VirtualBox
        # @return [Hash] - disk_metadata
        def self.handle_configure_disk(machine, disk, all_disks)
          disk_metadata = {}

          # Grab the existing configured disk, if it exists
          current_disk = get_current_disk(machine, disk, all_disks)

          # Configure current disk
          if !current_disk
            # create new disk and attach
            disk_metadata = create_disk(machine, disk)
          elsif compare_disk_size(machine, disk, current_disk)
            disk_metadata = resize_disk(machine, disk, current_disk)
          else
            disk_metadata = {UUID: current_disk["DiskIdentifier"], Name: disk.name, Path: current_disk["Path"]}
            if disk.primary
              disk_metadata[:primary] = true
            end
          end

          disk_metadata
        end

        def self.handle_configure_dvd(machine, dvd)
          dvd_location = File.expand_path(dvd.file)

          find_dvd_disk = proc {
            catch(:found) do
              machine.provider.driver.read_scsi_controllers.each do |controller|
                controller["Drives"].each do |disk|
                  throw :found, disk if File.expand_path(disk["Path"]) == dvd_location
                end
              end

              nil
            end
          }

          generate_dvd_metadata = proc { |disk|
            disk.slice(
              "Name", "Id", "Path",
              "ControllerLocation", "ControllerNumber",
              "ControllerType"
            )
          }

          # If the disk is already attached just
          # return the information
          if dvd_attached = find_dvd_disk.call
            return generate_dvd_metadata.call(dvd_attached)
          end

          # Attach the disk
          machine.provider.driver.attach_dvd(dvd_location)

          # Find disk and return information
          disk = find_dvd_disk.call
          return generate_dvd_metadata.call(disk) if disk

          LOGGER.warn("failed to locate dvd on controller, stubbing")
          {"Path" => dvd_location}
        end

        # Check to see if current disk is configured based on defined_disks
        #
        # @param [Kernel_V2::VagrantConfigDisk] disk_config
        # @param [Hash] defined_disk
        # @return [Boolean]
        def self.compare_disk_size(machine, disk_config, defined_disk)
          # Hyper-V returns disk size in bytes
          requested_disk_size = disk_config.size
          disk_actual = machine.provider.driver.get_disk(defined_disk["Path"])
          defined_disk_size = disk_actual["Size"]

          if defined_disk_size > requested_disk_size
            if File.extname(disk_actual["Path"]) == ".vhdx"
              # VHDX formats can be shrunk
              return true
            else
              machine.ui.warn(I18n.t("vagrant.cap.configure_disks.shrink_size_not_supported", name: disk_config.name))
              return false
            end
          elsif defined_disk_size < requested_disk_size
            return true
          else
            return false
          end
        end

        # Creates and attaches a disk to a machine
        #
        # @param [Vagrant::Machine] machine
        # @param [Kernel_V2::VagrantConfigDisk] disk_config
        def self.create_disk(machine, disk_config)
          machine.ui.detail(I18n.t("vagrant.cap.configure_disks.create_disk", name: disk_config.name))
          disk_provider_config = {}

          if disk_config.provider_config && disk_config.provider_config.key?(:hyperv)
            disk_provider_config = disk_config.provider_config[:hyperv]
          end

          if !disk_provider_config.empty?
            disk_provider_config = convert_size_vars!(disk_provider_config)
          end

          # Get the machines data dir, that will now be the path for the new disk
          guest_disk_folder = machine.data_dir.join("Virtual Hard Disks")

          if disk_config.file
            disk_file = disk_config.file
            LOGGER.info("Disk already defined by user at '#{disk_file}'. Using this disk instead of creating a new one...")
          else
            # Set the extension
            disk_ext = disk_config.disk_ext
            disk_file = File.join(guest_disk_folder, disk_config.name) + ".#{disk_ext}"

            LOGGER.info("Attempting to create a new disk file '#{disk_file}' of size '#{disk_config.size}' bytes")

            machine.provider.driver.create_disk(disk_file, disk_config.size, **disk_provider_config)
          end

          disk_info = machine.provider.driver.get_disk(disk_file)
          disk_metadata = {UUID: disk_info["DiskIdentifier"], Name: disk_config.name, Path: disk_info["Path"]}

          machine.provider.driver.attach_disk(disk_file, **disk_provider_config)

          disk_metadata
        end

        # Converts any "shortcut" options such as "123MB" into its byte form. This
        # is due to what parameter type is expected when calling the `New-VHD`
        # powershell command
        #
        # @param [Hash] disk_provider_config
        # @return [Hash] disk_provider_config
        def self.convert_size_vars!(disk_provider_config)
          if disk_provider_config.key?(:BlockSizeBytes)
            bytes = Vagrant::Util::Numeric.string_to_bytes(disk_provider_config[:BlockSizeBytes])
            disk_provider_config[:BlockSizeBytes] = bytes
          end

          disk_provider_config
        end

        # @param [Vagrant::Machine] machine
        # @param [Config::Disk] disk_config - the current disk to configure
        # @param [Hash] defined_disk - current disk as represented by VirtualBox
        # @return [Hash] - disk_metadata
        def self.resize_disk(machine, disk_config, defined_disk)
          machine.ui.detail(I18n.t("vagrant.cap.configure_disks.resize_disk", name: disk_config.name), prefix: true)

          machine.provider.driver.resize_disk(defined_disk["Path"], disk_config.size.to_i)

          disk_info = machine.provider.driver.get_disk(defined_disk["Path"])

          # Store updated metadata
          disk_metadata = {UUID: disk_info["DiskIdentifier"], Name: disk_config.name, Path: disk_info["Path"]}

          disk_metadata
        end
      end
    end
  end
end
