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

require 'fileutils'
require 'thread'
require 'zlib'

require "log4r"

require "vagrant/util/platform"

module VagrantPlugins
  module SyncedFolderNFS
    # This synced folder requires that two keys be set on the environment
    # within the middleware sequence:
    #
    #   - `:nfs_host_ip` - The IP of where to mount the NFS folder from.
    #   - `:nfs_machine_ip` - The IP of the machine where the NFS folder
    #     will be mounted.
    #
    class SyncedFolder < Vagrant.plugin("2", :synced_folder)
      @@lock = Mutex.new

      def initialize(*args)
        super

        @logger = Log4r::Logger.new("vagrant::synced_folders::nfs")
      end

      def usable?(machine, raise_error=false)
        # If the machine explicitly said NFS is not supported, then
        # it isn't supported.
        if !machine.config.nfs.functional
          return false
        end
        if machine.env.host.capability?(:nfs_installed)
          return true if machine.env.host.capability(:nfs_installed)
        end
        return false if !raise_error
        raise Vagrant::Errors::NFSNotSupported
      end

      def prepare(machine, folders, opts)
        # Nothing is necessary to do before VM boot.
      end

      def enable(machine, folders, nfsopts)
        raise Vagrant::Errors::NFSNoHostIP if !nfsopts[:nfs_host_ip]
        raise Vagrant::Errors::NFSNoGuestIP if !nfsopts[:nfs_machine_ip]

        if machine.config.nfs.verify_installed
          if machine.guest.capability?(:nfs_client_installed)
            installed = machine.guest.capability(:nfs_client_installed)
            if !installed
              can_install = machine.guest.capability?(:nfs_client_install)
              raise Vagrant::Errors::NFSClientNotInstalledInGuest if !can_install
              machine.ui.info I18n.t("vagrant.actions.vm.nfs.installing")
              machine.guest.capability(:nfs_client_install)
            end
          end
        end

        machine_ip = nfsopts[:nfs_machine_ip]
        machine_ip = [machine_ip] if !machine_ip.is_a?(Array)

        # Prepare the folder, this means setting up various options
        # and such on the folder itself.
        folders.each { |id, opts| prepare_folder(machine, opts) }

        # Determine what folders we'll export
        export_folders = folders.dup
        export_folders.keys.each do |id|
          opts = export_folders[id]
          if opts.key?(:nfs_export) && !opts[:nfs_export]
            export_folders.delete(id)
          end
        end

        # Update the exports when there are actually exports [GH-4148]
        if !export_folders.empty?
          # Export the folders. We do this with a class-wide lock because
          # NFS exporting often requires sudo privilege and we don't want
          # overlapping input requests. [GH-2680]
          @@lock.synchronize do
            begin
              machine.env.lock("nfs-export") do
                machine.ui.info I18n.t("vagrant.actions.vm.nfs.exporting")
                machine.env.host.capability(
                  :nfs_export,
                  machine.ui, machine.id, machine_ip, export_folders)
              end
            rescue Vagrant::Errors::EnvironmentLockedError
              sleep 1
              retry
            end
          end
        end

        # Mount
        machine.ui.info I18n.t("vagrant.actions.vm.nfs.mounting")

        # Only mount folders that have a guest path specified.
        mount_folders = {}
        folders.each do |id, opts|
          mount_folders[id] = opts.dup if opts[:guestpath]

          machine.ui.detail(I18n.t("vagrant.actions.vm.nfs.mounting_entry",
            guestpath: opts[:guestpath],
            hostpath: opts[:hostpath]
          ))
        end

        # Mount them!
        if machine.guest.capability?(:nfs_pre)
          machine.guest.capability(:nfs_pre)
        end

        machine.guest.capability(:mount_nfs_folder,
          nfsopts[:nfs_host_ip], mount_folders)

        if machine.guest.capability?(:nfs_post)
          machine.guest.capability(:nfs_post)
        end
      end

      def cleanup(machine, opts)
        ids = opts[:nfs_valid_ids]
        raise Vagrant::Errors::NFSNoValidIds if !ids

        # Prune any of the unused machines
        @logger.info("NFS pruning. Valid IDs: #{ids.inspect}")
        machine.env.host.capability(:nfs_prune, machine.ui, ids)
      end

      protected

      def prepare_folder(machine, opts)
        opts[:map_uid] = prepare_permission(machine, :uid, opts)
        opts[:map_gid] = prepare_permission(machine, :gid, opts)
        opts[:nfs_version] ||= 3
        if !opts.key?(:nfs_udp)
          opts[:nfs_udp] = !opts[:nfs_version].to_s.start_with?('4')
        end

        if opts[:nfs_version].to_s.start_with?('4') && opts[:nfs_udp]
          machine.ui.info I18n.t("vagrant.actions.vm.nfs.v4_with_udp_warning")
        end

        # We use a CRC32 to generate a 32-bit checksum so that the
        # fsid is compatible with both old and new kernels.
        opts[:uuid] = Zlib.crc32(opts[:hostpath]).to_s
      end

      # Prepares the UID/GID settings for a single folder.
      def prepare_permission(machine, perm, opts)
        key = "map_#{perm}".to_sym
        return nil if opts.key?(key) && opts[key].nil?

        # The options on the hash get priority, then the default
        # values
        value = opts.key?(key) ? opts[key] : machine.config.nfs.send(key)
        return value if value != :auto

        # Get UID/GID from folder if we've made it this far
        # (value == :auto)
        stat = File.stat(opts[:hostpath])
        return stat.send(perm)
      end
    end
  end
end
