# frozen_string_literal: true

require 'pathname'
require_relative '../../puppet/file_bucket'
require_relative '../../puppet/file_bucket/file'
require_relative '../../puppet/indirector/request'
require_relative '../../puppet/util/diff'
require 'tempfile'

class Puppet::FileBucket::Dipper
  include Puppet::Util::Checksums
  # This is a transitional implementation that uses REST
  # to access remote filebucket files.

  attr_accessor :name

  # Creates a bucket client
  def initialize(hash = {})
    # Emulate the XMLRPC client
    server      = hash[:Server]
    port        = hash[:Port] || Puppet[:serverport]

    if hash.include?(:Path)
      @local_path = hash[:Path]
      @rest_path  = nil
    else
      @local_path = nil
      @rest_path = "filebucket://#{server}:#{port}/"
    end
    @checksum_type = Puppet[:digest_algorithm].to_sym
    @digest = method(@checksum_type)
  end

  def local?
    !!@local_path
  end

  # Backs up a file to the file bucket
  def backup(file)
    file_handle = Puppet::FileSystem.pathname(file)
    raise(ArgumentError, _("File %{file} does not exist") % { file: file }) unless Puppet::FileSystem.exist?(file_handle)

    begin
      file_bucket_file = Puppet::FileBucket::File.new(file_handle, :bucket_path => @local_path)
      files_original_path = absolutize_path(file)
      dest_path = "#{@rest_path}#{file_bucket_file.name}/#{files_original_path}"
      file_bucket_path = "#{@rest_path}#{file_bucket_file.checksum_type}/#{file_bucket_file.checksum_data}/#{files_original_path}"

      # Make a HEAD request for the file so that we don't waste time
      # uploading it if it already exists in the bucket.
      unless Puppet::FileBucket::File.indirection.head(file_bucket_path, :bucket_path => file_bucket_file.bucket_path)
        Puppet::FileBucket::File.indirection.save(file_bucket_file, dest_path)
      end

      file_bucket_file.checksum_data
    rescue => detail
      message = _("Could not back up %{file}: %{detail}") % { file: file, detail: detail }
      Puppet.log_exception(detail, message)
      raise Puppet::Error, message, detail.backtrace
    end
  end

  # Diffs two filebucket files identified by their sums
  def diff(checksum_a, checksum_b, file_a, file_b)
    raise RuntimeError, _("Diff is not supported on this platform") if Puppet[:diff] == ""

    if checksum_a
      source_path = "#{@rest_path}#{@checksum_type}/#{checksum_a}"
      if checksum_b
        file_diff = Puppet::FileBucket::File.indirection.find(
          source_path,
          :bucket_path => @local_path,
          :diff_with => checksum_b
        )
      elsif file_b
        tmp_file = ::Tempfile.new('diff')
        begin
          restore(tmp_file.path, checksum_a)
          file_diff = Puppet::Util::Diff.diff(tmp_file.path, file_b)
        ensure
          tmp_file.close
          tmp_file.unlink
        end
      else
        raise Puppet::Error, _("Please provide a file or checksum to diff with")
      end
    elsif file_a
      if checksum_b
        tmp_file = ::Tempfile.new('diff')
        begin
          restore(tmp_file.path, checksum_b)
          file_diff = Puppet::Util::Diff.diff(file_a, tmp_file.path)
        ensure
          tmp_file.close
          tmp_file.unlink
        end
      elsif file_b
        file_diff = Puppet::Util::Diff.diff(file_a, file_b)
      end
    end
    raise Puppet::Error, _("Failed to diff files") unless file_diff

    file_diff.to_s
  end

  # Retrieves a file by sum.
  def getfile(sum)
    get_bucket_file(sum).to_s
  end

  # Retrieves a FileBucket::File by sum.
  def get_bucket_file(sum)
    source_path = "#{@rest_path}#{@checksum_type}/#{sum}"
    file_bucket_file = Puppet::FileBucket::File.indirection.find(source_path, :bucket_path => @local_path)

    raise Puppet::Error, _("File not found") unless file_bucket_file

    file_bucket_file
  end

  # Restores the file
  def restore(file, sum)
    restore = true
    file_handle = Puppet::FileSystem.pathname(file)
    if Puppet::FileSystem.exist?(file_handle)
      cursum = Puppet::FileBucket::File.new(file_handle).checksum_data()

      # if the checksum has changed...
      # this might be extra effort
      if cursum == sum
        restore = false
      end
    end

    if restore
      newcontents = get_bucket_file(sum)
      if newcontents
        newsum = newcontents.checksum_data
        changed = nil
        if Puppet::FileSystem.exist?(file_handle) and !Puppet::FileSystem.writable?(file_handle)
          changed = Puppet::FileSystem.stat(file_handle).mode
          ::File.chmod(changed | 0o200, file)
        end
        ::File.open(file, ::File::WRONLY | ::File::TRUNC | ::File::CREAT) { |of|
          of.binmode
          newcontents.stream do |source_stream|
            FileUtils.copy_stream(source_stream, of)
          end
        }
        ::File.chmod(changed, file) if changed
      else
        Puppet.err _("Could not find file with checksum %{sum}") % { sum: sum }
        return nil
      end
      newsum
    else
      nil
    end
  end

  # List Filebucket content.
  def list(fromdate, todate)
    raise Puppet::Error, _("Listing remote file buckets is not allowed") unless local?

    source_path = "#{@rest_path}#{@checksum_type}/"
    file_bucket_list = Puppet::FileBucket::File.indirection.find(
      source_path,
      :bucket_path => @local_path,
      :list_all => true,
      :fromdate => fromdate,
      :todate => todate
    )
    raise Puppet::Error, _("File not found") unless file_bucket_list

    file_bucket_list.to_s
  end

  private

  def absolutize_path(path)
    Pathname.new(path).realpath
  end
end
