# frozen_string_literal: true

require 'fileutils'
require 'log4r'

require 'vagrant-libvirt/util/ui'
require 'vagrant-libvirt/util/unindent'

module VagrantPlugins
  module ProviderLibvirt
    module Action
      # Action for create new box for Libvirt provider
      class PackageDomain
        include VagrantPlugins::ProviderLibvirt::Util::Ui


        def initialize(app, env)
          @logger = Log4r::Logger.new('vagrant_libvirt::action::package_domain')
          @app = app

          @options = ENV.fetch('VAGRANT_LIBVIRT_VIRT_SYSPREP_OPTIONS', '')
          @operations = ENV.fetch('VAGRANT_LIBVIRT_VIRT_SYSPREP_OPERATIONS', 'defaults,-ssh-userdir,-ssh-hostkeys,-customize')
        end

        def call(env)
          env[:ui].info(I18n.t('vagrant_libvirt.package_domain'))
          libvirt_domain = env[:machine].provider.driver.connection.client.lookup_domain_by_uuid(
            env[:machine].id
          )
          domain = env[:machine].provider.driver.connection.servers.get(env[:machine].id.to_s)

          volumes = domain.volumes.select { |x| !x.nil? }
          root_disk = volumes.select do |x|
            x.name == libvirt_domain.name + '.img'
          end.first
          raise Errors::NoDomainVolume if root_disk.nil?

          package_func = method(:package_v1)

          box_format = ENV.fetch('VAGRANT_LIBVIRT_BOX_FORMAT_VERSION', nil)

          case box_format
          when nil
            if volumes.length() > 1
              msg = "Detected more than one volume for machine, in the future this will switch to using the v2 "
              msg += "box format v2 automatically."
              msg += "\nIf you want to include the additional disks attached when packaging please set the "
              msg += "env variable VAGRANT_LIBVIRT_BOX_FORMAT_VERSION=v2 to use the new format. If you want "
              msg += "to ensure that your box uses the old format for single disk only, please set the "
              msg += "environment variable explicitly to 'v1'"
              env[:ui].warn(msg)
            end
          when 'v2'
            package_func = method(:package_v2)
          when 'v1'
          else
            env[:ui].warn("Unrecognized value for 'VAGRANT_LIBVIRT_BOX_FORMAT_VERSION', defaulting to v1")
          end

          metadata = package_func.call(env, volumes)

          # metadata / Vagrantfile
          package_directory = env["package.directory"]
          File.write(package_directory + '/metadata.json', metadata)
          File.write(package_directory + '/Vagrantfile', vagrantfile_content(env))

          @app.call(env)
        end

        def package_v1(env, volumes)
          domain_img = download_volume(env, volumes.first, 'box.img')

          sysprep_domain(domain_img)
          sparsify_volume(domain_img)

          info = JSON.parse(`qemu-img info --output=json #{domain_img}`)
          img_size = (Float(info['virtual-size'])/(1024**3)).ceil

          return metadata_content_v1(img_size)
        end

        def package_v2(env, volumes)
          disks = []
          volumes.each_with_index do |vol, idx|
            disk = {:path => "box_#{idx+1}.img"}
            volume_img = download_volume(env, vol, disk[:path])

            if idx == 0
              sysprep_domain(volume_img)
            end

            sparsify_volume(volume_img)

            disks.push(disk)
          end

          return metadata_content_v2(disks)
        end

        def vagrantfile_content(env)
          include_vagrantfile = ""

          if env["package.vagrantfile"]
            include_vagrantfile = <<-EOF

              # Load include vagrant file if it exists after the auto-generated
              # so it can override any of the settings
              include_vagrantfile = File.expand_path("../include/_Vagrantfile", __FILE__)
              load include_vagrantfile if File.exist?(include_vagrantfile)
            EOF
          end

          <<-EOF.unindent
            Vagrant.configure("2") do |config|
              config.vm.provider :libvirt do |libvirt|
                libvirt.driver = "kvm"
              end
            #{include_vagrantfile}
            end
          EOF
        end

        def metadata_content_v1(filesize)
          <<-EOF.unindent
            {
              "provider": "libvirt",
              "format": "qcow2",
              "virtual_size": #{filesize}
            }
          EOF
        end

        def metadata_content_v2(disks)
          data = {
            "provider": "libvirt",
            "format": "qcow2",
            "disks": disks.each do |disk|
              {'path': disk[:path]}
            end
          }
          JSON.pretty_generate(data)
        end

        protected

        def sparsify_volume(volume_img)
          `virt-sparsify --in-place #{volume_img}`
        end

        def sysprep_domain(domain_img)
          # remove hw association with interface
          # working for centos with lvs default disks
          `virt-sysprep --no-logfile --operations #{@operations} -a #{domain_img} #{@options}`
        end

        def download_volume(env, volume, disk_path)
          package_directory = env["package.directory"]
          volume_img = package_directory + '/' + disk_path
          env[:ui].info("Downloading #{volume.name} to #{volume_img}")
          download_image(volume_img, env[:machine].provider_config.storage_pool_name,
                               volume.name, env) do |progress,image_size|
            rewriting(env[:ui]) do |ui|
              ui.clear_line
              ui.report_progress(progress, image_size, false)
            end
          end
          # Clear the line one last time since the progress meter doesn't
          # disappear immediately.
          rewriting(env[:ui]) {|ui| ui.clear_line}

          # Prep domain disk
          backing = `qemu-img info "#{volume_img}" | grep 'backing file:' | cut -d ':' -f2`.chomp
          if backing
            env[:ui].info('Image has backing image, copying image and rebasing ...')
            `qemu-img rebase -p -b "" #{volume_img}`
          end

          return volume_img
        end

        # Fog libvirt currently doesn't support downloading images from storage
        # pool volumes. Use ruby-libvirt client instead.
        def download_image(image_file, pool_name, volume_name, env)
          begin
            pool = env[:machine].provider.driver.connection.client.lookup_storage_pool_by_name(
              pool_name
            )
            volume = pool.lookup_volume_by_name(volume_name)
            image_size = volume.info.allocation # B

            stream = env[:machine].provider.driver.connection.client.stream

            # Use length of 0 to download remaining contents after offset
            volume.download(stream, offset = 0, length = 0)

            buf_size = 1024 * 250 # 250K, copied from upload_image in handle_box_image.rb
            progress = 0
            retval = stream.recv(buf_size)
            open(image_file, 'wb') do |io|
              while (retval.at(0) > 0)
                recvd = io.write(retval.at(1))
                progress += recvd
                yield [progress, image_size]
                retval = stream.recv(buf_size)
              end
            end
          rescue => e
            raise Errors::ImageDownloadError,
                  volume_name: volume_name,
                  pool_name: pool_name,
                  error_message: e.message
          end

          progress == image_size
        end
      end
    end
  end
end
