# frozen_string_literal: true

require 'pathname'
require 'tmpdir'
require_relative '../../../puppet/util/json'
require_relative '../../../puppet/file_system'

module Puppet::ModuleTool
  module Applications
    class Unpacker < Application
      def self.unpack(filename, target)
        app = new(filename, :target_dir => target)
        app.unpack
        app.sanity_check
        app.move_into(target)
      end

      def self.harmonize_ownership(source, target)
        unless Puppet::Util::Platform.windows?
          source = Pathname.new(source) unless source.respond_to?(:stat)
          target = Pathname.new(target) unless target.respond_to?(:stat)

          FileUtils.chown_R(source.stat.uid, source.stat.gid, target)
        end
      end

      def initialize(filename, options = {})
        @filename = Pathname.new(filename)
        super(options)
        @module_path = Pathname(options[:target_dir])
      end

      def run
        unpack
        sanity_check
        module_dir = @module_path + module_name
        move_into(module_dir)

        # Return the Pathname object representing the directory where the
        # module release archive was unpacked the to.
        module_dir
      end

      # @api private
      # Error on symlinks and other junk
      def sanity_check
        symlinks = Dir.glob("#{tmpdir}/**/*", File::FNM_DOTMATCH).map { |f| Pathname.new(f) }.select { |p| Puppet::FileSystem.symlink? p }
        tmpdirpath = Pathname.new tmpdir

        symlinks.each do |s|
          Puppet.warning _("Symlinks in modules are unsupported. Please investigate symlink %{from}->%{to}.") % { from: s.relative_path_from(tmpdirpath), to: Puppet::FileSystem.readlink(s) }
        end
      end

      # @api private
      def unpack
        Puppet::ModuleTool::Tar.instance.unpack(@filename.to_s, tmpdir, [@module_path.stat.uid, @module_path.stat.gid].join(':'))
      rescue Puppet::ExecutionFailure => e
        raise RuntimeError, _("Could not extract contents of module archive: %{message}") % { message: e.message }
      end

      # @api private
      def root_dir
        return @root_dir if @root_dir

        # Grab the first directory containing a metadata.json file
        metadata_file = Dir["#{tmpdir}/**/metadata.json"].min_by(&:length)

        if metadata_file
          @root_dir = Pathname.new(metadata_file).dirname
        else
          raise _("No valid metadata.json found!")
        end
      end

      # @api private
      def module_name
        metadata = Puppet::Util::Json.load((root_dir + 'metadata.json').read)
        metadata['name'][/-(.*)/, 1]
      end

      # @api private
      def move_into(dir)
        dir = Pathname.new(dir)
        dir.rmtree if dir.exist?
        FileUtils.mv(root_dir, dir)
      ensure
        FileUtils.rmtree(tmpdir)
      end

      # Obtain a suitable temporary path for unpacking tarballs
      #
      # @api private
      # @return [String] path to temporary unpacking location
      # rubocop:disable Naming/MemoizedInstanceVariableName
      def tmpdir
        @dir ||= Dir.mktmpdir('tmp', Puppet::Forge::Cache.base_path)
      end
      # rubocop:enable Naming/MemoizedInstanceVariableName
    end
  end
end
